MEECROWAVE-197 very trivial proxy module, more to come

git-svn-id: https://svn.apache.org/repos/asf/openwebbeans/meecrowave/trunk@1860778 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/meecrowave-doc/pom.xml b/meecrowave-doc/pom.xml
index 496e21c..348b665 100644
--- a/meecrowave-doc/pom.xml
+++ b/meecrowave-doc/pom.xml
@@ -102,6 +102,11 @@
       <version>${project.version}</version>
     </dependency>
     <dependency>
+      <groupId>org.apache.meecrowave</groupId>
+      <artifactId>meecrowave-proxy</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
       <groupId>org.jbake</groupId>
       <artifactId>jbake-core</artifactId>
       <version>2.6.4</version>
diff --git a/meecrowave-doc/src/main/java/org/apache/meecrowave/doc/JBake.java b/meecrowave-doc/src/main/java/org/apache/meecrowave/doc/JBake.java
index 7dd8328..75801f9 100755
--- a/meecrowave-doc/src/main/java/org/apache/meecrowave/doc/JBake.java
+++ b/meecrowave-doc/src/main/java/org/apache/meecrowave/doc/JBake.java
@@ -53,6 +53,7 @@
 import org.apache.meecrowave.doc.generator.LetsEncryptConfiguration;

 import org.apache.meecrowave.doc.generator.MavenConfiguration;

 import org.apache.meecrowave.doc.generator.OAuth2Configuration;

+import org.apache.meecrowave.doc.generator.ProxyConfiguration;

 import org.jbake.app.Oven;

 import org.jbake.app.configuration.ConfigUtil;

 import org.jbake.app.configuration.DefaultJBakeConfiguration;

@@ -83,6 +84,7 @@
         new OAuth2Configuration().run();

         new LetsEncryptConfiguration().run();

         new GradleConfiguration().run();

+        new ProxyConfiguration().run();

 

         if (updateDownloads) {

             final ByteArrayOutputStream tableContent = new ByteArrayOutputStream();

diff --git a/meecrowave-doc/src/main/java/org/apache/meecrowave/doc/generator/ProxyConfiguration.java b/meecrowave-doc/src/main/java/org/apache/meecrowave/doc/generator/ProxyConfiguration.java
new file mode 100644
index 0000000..b98a42c
--- /dev/null
+++ b/meecrowave-doc/src/main/java/org/apache/meecrowave/doc/generator/ProxyConfiguration.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.meecrowave.doc.generator;
+
+import static java.util.stream.Collectors.joining;
+
+import java.lang.reflect.Field;
+import java.util.Comparator;
+import java.util.stream.Stream;
+
+import org.apache.meecrowave.proxy.servlet.meecrowave.ProxyServletSetup;
+import org.apache.meecrowave.runner.cli.CliOption;
+
+public class ProxyConfiguration extends BaseGenerator {
+    @Override
+    protected String generate() {
+        return super.tableConfig() + "|===\n|Name|Description\n" +
+                Stream.of(ProxyServletSetup.Configuration.class.getDeclaredFields())
+                        .filter(f -> f.isAnnotationPresent(CliOption.class))
+                        .sorted(Comparator.comparing(Field::getName))
+                        .map(f -> f.getAnnotation(CliOption.class))
+                        .map(opt -> "|--" + opt.name() + "|" + opt.description())
+                        .collect(joining("\n")) + "\n|===\n";
+    }
+}
diff --git a/meecrowave-doc/src/main/jbake/content/meecrowave-proxy/index.adoc b/meecrowave-doc/src/main/jbake/content/meecrowave-proxy/index.adoc
new file mode 100644
index 0000000..de62d4c
--- /dev/null
+++ b/meecrowave-doc/src/main/jbake/content/meecrowave-proxy/index.adoc
@@ -0,0 +1,33 @@
+= Meecrowave Proxy
+:jbake-date: 2019-06-07
+:jbake-type: page
+:jbake-status: published
+:jbake-meecrowavepdf:
+:jbake-meecrowavetitleicon: icon icon_puzzle_alt
+:jbake-meecrowavecolor: blue-green
+:icons: font
+
+Coordinates:
+
+[source,xml]
+----
+<dependency>
+  <groupId>org.apache.meecrowave</groupId>
+  <artifactId>meecrowave-proxy</artifactId>
+  <version>${meecrowave.version}</version>
+</dependency>
+----
+
+Simple proxy module using Meecrowave as backbone.
+It can be extended using CDI programming model and JAX-RS client.
+
+== Configuration
+
+include::../../../../../target/generated-doc/ProxyConfiguration.adoc[]
+
+TIP: you can use that servlet in a plain Servlet container (adding JAX-RS+JSON-B client).
+An integration example can be found in `org.apache.meecrowave.proxy.servlet.meecrowave.ProxyServletSetup#accept`.
+
+== Extend
+
+TBD
diff --git a/meecrowave-proxy/meecrowave-proxy.iml b/meecrowave-proxy/meecrowave-proxy.iml
new file mode 100644
index 0000000..0c9a3ce
--- /dev/null
+++ b/meecrowave-proxy/meecrowave-proxy.iml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
+  <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
+    <output url="file://$MODULE_DIR$/target/classes" />
+    <output-test url="file://$MODULE_DIR$/target/test-classes" />
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
+      <excludeFolder url="file://$MODULE_DIR$/target" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+    <orderEntry type="module" module-name="meecrowave-specs-api" />
+    <orderEntry type="library" name="Maven: org.apache.geronimo.specs:geronimo-annotation_1.3_spec:1.1" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.geronimo.specs:geronimo-jcdi_2.0_spec:1.1" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.geronimo.specs:geronimo-atinject_1.0_spec:1.1" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.geronimo.specs:geronimo-interceptor_1.2_spec:1.1" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.geronimo.specs:geronimo-json_1.1_spec:1.1" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.geronimo.specs:geronimo-jsonb_1.0_spec:1.1" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.geronimo.specs:geronimo-jaxrs_2.1_spec:1.1" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.tomcat:tomcat-servlet-api:9.0.20" level="project" />
+    <orderEntry type="module" module-name="meecrowave-core" scope="PROVIDED" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.tomcat:tomcat-jaspic-api:9.0.20" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.xbean:xbean-finder-shaded:4.14" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.xbean:xbean-asm7-shaded:4.14" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.xbean:xbean-reflect:4.14" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.openwebbeans:openwebbeans-web:2.0.11" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.openwebbeans:openwebbeans-impl:2.0.11" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.openwebbeans:openwebbeans-spi:2.0.11" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.openwebbeans:openwebbeans-el22:2.0.11" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.tomcat:tomcat-catalina:9.0.20" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.tomcat:tomcat-juli:9.0.20" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.tomcat:tomcat-api:9.0.20" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.tomcat:tomcat-jni:9.0.20" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.tomcat:tomcat-coyote:9.0.20" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.tomcat:tomcat-util:9.0.20" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.tomcat:tomcat-util-scan:9.0.20" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.cxf:cxf-rt-frontend-jaxrs:3.3.2" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.cxf:cxf-core:3.3.2" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.cxf:cxf-rt-transports-http:3.3.2" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.cxf:cxf-rt-security:3.3.2" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.cxf:cxf-integration-cdi:3.3.2" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.cxf:cxf-rt-rs-client:3.3.2" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.johnzon:johnzon-jaxrs:1.1.12" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.johnzon:johnzon-mapper:1.1.12" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.johnzon:johnzon-core:1.1.12" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.johnzon:johnzon-jsonb:1.1.12" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.logging.log4j:log4j-api:2.11.2" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.logging.log4j:log4j-core:2.11.2" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.apache.logging.log4j:log4j-jul:2.11.2" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.13-beta-3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" />
+    <orderEntry type="module" module-name="meecrowave-junit" scope="TEST" />
+  </component>
+</module>
\ No newline at end of file
diff --git a/meecrowave-proxy/pom.xml b/meecrowave-proxy/pom.xml
new file mode 100644
index 0000000..75dc8a2
--- /dev/null
+++ b/meecrowave-proxy/pom.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <parent>
+    <artifactId>meecrowave</artifactId>
+    <groupId>org.apache.meecrowave</groupId>
+    <version>1.2.9-SNAPSHOT</version>
+  </parent>
+  <modelVersion>4.0.0</modelVersion>
+
+  <artifactId>meecrowave-proxy</artifactId>
+  <name>Meecrowave :: Proxy</name>
+
+  <properties>
+    <meecrowave.build.name>${project.groupId}.proxy</meecrowave.build.name>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.meecrowave</groupId>
+      <artifactId>meecrowave-specs-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.meecrowave</groupId>
+      <artifactId>meecrowave-core</artifactId>
+      <version>${project.version}</version>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>${junit.version}</version>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.meecrowave</groupId>
+      <artifactId>meecrowave-junit</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/configuration/Routes.java b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/configuration/Routes.java
new file mode 100644
index 0000000..9c0d71c
--- /dev/null
+++ b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/configuration/Routes.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.meecrowave.proxy.servlet.configuration;
+
+import java.util.Collection;
+
+public class Routes {
+    public Route defaultRoute;
+    public Collection<Route> routes;
+
+    @Override
+    public String toString() {
+        return "Routes{routes=" + routes + '}';
+    }
+
+    public static class Route {
+        public String id;
+        public RequestConfiguration requestConfiguration;
+        public ResponseConfiguration responseConfiguration;
+
+        @Override
+        public String toString() {
+            return "Route{id='" + id + "', requestConfiguration=" + requestConfiguration + ", responseConfiguration=" + responseConfiguration + '}';
+        }
+    }
+
+    public static class ResponseConfiguration {
+        public String target;
+        public Collection<String> skippedHeaders;
+        public Collection<String> skippedCookies;
+
+        @Override
+        public String toString() {
+            return "ResponseConfiguration{target='" + target + "'}";
+        }
+    }
+
+    public static class RequestConfiguration {
+        public String method;
+        public String prefix;
+        public Collection<String> skippedHeaders;
+        public Collection<String> skippedCookies;
+
+        @Override
+        public String toString() {
+            return "RequestConfiguration{method='" + method + "', prefix='" + prefix + "'}";
+        }
+    }
+}
diff --git a/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/ProxyServlet.java b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/ProxyServlet.java
new file mode 100644
index 0000000..3eab49c
--- /dev/null
+++ b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/ProxyServlet.java
@@ -0,0 +1,323 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.meecrowave.proxy.servlet.front;
+
+import static java.util.Collections.list;
+import static java.util.Optional.empty;
+import static java.util.Optional.ofNullable;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.stream.Collectors.toMap;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.util.AbstractMap;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.stream.Stream;
+
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.CompletionStageRxInvoker;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.Invocation;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.NewCookie;
+import javax.ws.rs.core.Response;
+
+import org.apache.meecrowave.proxy.servlet.configuration.Routes;
+import org.apache.meecrowave.proxy.servlet.service.ConfigurationLoader;
+
+// IMPORTANT: don't make this class depending on meecrowave, cxf or our internals, use setup class
+public class ProxyServlet extends HttpServlet {
+    protected Routes routes;
+    protected Client client;
+    protected ExecutorService executor;
+    protected long awaitTimeout;
+    protected long asyncTimeout;
+    protected int prefixLength;
+
+    @Override
+    protected void service(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+        final String prefix = req.getRequestURI().substring(prefixLength);
+        final Optional<Routes.Route> matchedRoute = findRoute(req, prefix);
+        if (!matchedRoute.isPresent()) {
+            super.service(req, resp);
+        } else {
+            doExecute(matchedRoute.orElseThrow(IllegalArgumentException::new), req, resp, prefix);
+        }
+    }
+
+    protected CompletionStage<HttpServletResponse> doExecute(final Routes.Route route, final HttpServletRequest req, final HttpServletResponse resp,
+                                                             final String prefix) throws IOException {
+        final AsyncContext asyncContext = req.startAsync();
+        asyncContext.setTimeout(asyncTimeout);
+
+        WebTarget target = client.target(route.responseConfiguration.target);
+        target = target.path(prefix); // todo: query params, multipart, etc
+
+        final Map<String, String> queryParams = ofNullable(req.getQueryString())
+                .filter(it -> !it.isEmpty())
+                .map(queries -> Stream.of(queries.split("&"))
+                        .map(it -> {
+                            final int eq = it.indexOf('=');
+                            if (eq > 0) {
+                                return new AbstractMap.SimpleEntry<>(it.substring(0, eq), it.substring(eq + 1));
+                            }
+                            return new AbstractMap.SimpleEntry<>(it, "");
+                        })
+                        .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)))
+                .orElseGet(Collections::emptyMap);
+        for (final Map.Entry<String, String> q : queryParams.entrySet()) {
+            target = target.queryParam(q.getKey(), q.getValue());
+        }
+
+        final String type = req.getContentType();
+        final Invocation.Builder request = type != null ? target.request(type) : target.request();
+
+        final Enumeration<String> headerNames = req.getHeaderNames();
+        while (headerNames.hasMoreElements()) {
+            final String name = headerNames.nextElement();
+            if (!filterHeader(route.requestConfiguration.skippedHeaders, name)) {
+                request.header(name, list(req.getHeaders(name)));
+            }
+        }
+
+        final Cookie[] cookies = req.getCookies();
+        if (cookies != null) {
+            Stream.of(cookies)
+                    .filter(it -> filterCookie(route.requestConfiguration.skippedCookies, it.getName(), it.getValue()))
+                    .forEach(cookie -> request.cookie(
+                        new javax.ws.rs.core.Cookie(cookie.getName(), cookie.getValue(), cookie.getPath(), cookie.getDomain(), cookie.getVersion())));
+        }
+
+        final CompletionStageRxInvoker rx = request.rx();
+        final CompletionStage<Response> result;
+        if (isWrite(req)) {
+            result = rx.method(req.getMethod(), Entity.entity(req.getInputStream(), ofNullable(req.getContentType()).orElse(MediaType.WILDCARD)));
+        } else {
+            result = rx.method(req.getMethod());
+        }
+        return result.handle((response, error) -> {
+            try {
+                if (error != null) {
+                    onError(route, resp, error);
+                } else {
+                    try {
+                        forwardResponse(route, response, resp);
+                    } catch (final IOException e) {
+                        onError(route, resp, e);
+                    }
+                }
+            } catch (final IOException ioe) {
+                getServletContext().log("Error Proxying " + req.getMethod() + " " + req.getRequestURI() + ": " + ioe.getMessage(), ioe);
+            } finally {
+                asyncContext.complete();
+            }
+            return resp;
+        });
+    }
+
+    protected boolean isWrite(final HttpServletRequest req) {
+        return !HttpMethod.HEAD.equalsIgnoreCase(req.getMethod()) && !HttpMethod.GET.equalsIgnoreCase(req.getMethod());
+    }
+
+    protected void onError(final Routes.Route route, final HttpServletResponse resp, final Throwable error) throws IOException {
+        if (WebApplicationException.class.isInstance(error)) {
+            final WebApplicationException wae = WebApplicationException.class.cast(error);
+            if (wae.getResponse() != null) {
+                forwardResponse(route, wae.getResponse(), resp);
+                return;
+            }
+        }
+        onDefaultError(resp, error);
+    }
+
+    protected void onDefaultError(HttpServletResponse resp, Throwable error) throws IOException {
+        resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+        error.printStackTrace(new PrintWriter(resp.getOutputStream()));
+    }
+
+    protected void forwardResponse(final Routes.Route route, final Response response, final HttpServletResponse resp) throws IOException {
+        final int status = response.getStatus();
+        resp.setStatus(status);
+        forwardHeaders(route, response, resp);
+        if (status == HttpServletResponse.SC_NOT_MODIFIED && resp.getHeader(HttpHeaders.CONTENT_LENGTH) == null) {
+            resp.setIntHeader(HttpHeaders.CONTENT_LENGTH, 0);
+        }
+        forwardCookies(route, response, resp);
+        writeOutput(resp, response.readEntity(InputStream.class));
+    }
+
+    protected void forwardCookies(final Routes.Route route, final Response response, final HttpServletResponse resp) {
+        response.getCookies().entrySet().stream()
+                .filter(cookie -> filterCookie(route.requestConfiguration.skippedCookies, cookie.getKey(), cookie.getValue().getValue()))
+                .forEach(cookie -> addCookie(resp, cookie));
+    }
+
+    protected void addCookie(final HttpServletResponse resp, final Map.Entry<String, NewCookie> cookie) {
+        final NewCookie nc = cookie.getValue();
+        final Cookie servletCookie = new Cookie(cookie.getKey(), nc.getValue());
+        servletCookie.setComment(nc.getComment());
+        servletCookie.setDomain(nc.getDomain());
+        servletCookie.setHttpOnly(nc.isHttpOnly());
+        servletCookie.setSecure(nc.isSecure());
+        servletCookie.setMaxAge(nc.getMaxAge());
+        servletCookie.setPath(nc.getPath());
+        servletCookie.setVersion(nc.getVersion());
+        resp.addCookie(servletCookie);
+    }
+
+    protected void forwardHeaders(final Routes.Route route, final Response response, final HttpServletResponse resp) {
+        response.getHeaders().entrySet().stream()
+                .filter(header -> filterHeader(route.requestConfiguration.skippedHeaders, header.getKey()))
+                .flatMap(entry -> entry.getValue().stream().map(value -> new AbstractMap.SimpleEntry<>(entry.getKey(), String.valueOf(value))))
+                .forEach(header -> resp.addHeader(header.getKey(), header.getValue()));
+    }
+
+    protected boolean filterCookie(final Collection<String> blacklist, final String name, final String value) {
+        return value != null && (blacklist == null || blacklist.stream().anyMatch(it -> it.equalsIgnoreCase(name)));
+    }
+
+    protected boolean filterHeader(final Collection<String> blacklist, final String name) {
+        return  blacklist == null || blacklist.stream().anyMatch(it -> it.equalsIgnoreCase(name));
+    }
+
+    private void writeOutput(final HttpServletResponse resp, final InputStream stream) throws IOException {
+        final int bufferSize = Math.max(1, Math.min(8192, stream.available()));
+        final byte[] buffer = new byte[bufferSize]; // todo: reusable (copier?)
+        final ServletOutputStream outputStream = resp.getOutputStream();
+        int read;
+        while ((read = stream.read(buffer)) >= 0) {
+            if (read > 0) {
+                outputStream.write(buffer, 0, read);
+            }
+        }
+    }
+
+    protected Optional<Routes.Route> findRoute(final HttpServletRequest req, final String prefix) {
+        return routes == null ? empty() : routes.routes.stream()
+                .filter(it -> it.requestConfiguration.method == null || it.requestConfiguration.method.equalsIgnoreCase(req.getMethod()))
+                .filter(it -> it.requestConfiguration.prefix == null || it.requestConfiguration.prefix.equalsIgnoreCase(prefix))
+                .findFirst();
+    }
+
+    protected Optional<Routes> loadConfiguration() {
+        return get("configuration").flatMap(path -> new ConfigurationLoader(path).load());
+    }
+
+    @Override
+    public void init(final ServletConfig config) throws ServletException {
+        super.init(config);
+
+        prefixLength = get("mapping")
+                .map(it -> it.endsWith("/*") ? it.substring(0, it.length() - "/*".length()) : it)
+                .orElse("").length() + config.getServletContext().getContextPath().length();
+
+        awaitTimeout = getLong("shutdown.timeout").orElse(1L);
+        asyncTimeout = getLong("async.timeout").orElse(30000L);
+
+        setupClient();
+
+        final Optional<Routes> configuration = loadConfiguration();
+        if (!configuration.isPresent()) {
+            return;
+        }
+        routes = configuration.orElseThrow(IllegalArgumentException::new);
+    }
+
+    @Override
+    public void destroy() {
+        if (executor != null) {
+            executor.shutdown();
+            try {
+                if (!executor.awaitTermination(awaitTimeout, MILLISECONDS)) {
+                    getServletContext().log("Can't shutdown the client executor in " + awaitTimeout + "ms");
+                }
+            } catch (final InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+        }
+        client.close();
+        super.destroy();
+    }
+
+    protected void setupClient() {
+        executor = new ThreadPoolExecutor(
+                getInt("executor.core").orElse(64),
+                getInt("executor.max").orElse(512),
+                getLong("executor.keepAlive").orElse(60000L),
+                MILLISECONDS,
+                new LinkedBlockingQueue<>(),
+                new ThreadFactory() {
+                    private final SecurityManager sm = System.getSecurityManager();
+                    private final ThreadGroup group = (sm != null) ? sm.getThreadGroup() : Thread.currentThread().getThreadGroup();
+
+                    @Override
+                    public Thread newThread(final Runnable r) {
+                        final Thread newThread = new Thread(group, r, ProxyServlet.class.getName() + "_" + hashCode());
+                        newThread.setDaemon(false);
+                        newThread.setPriority(Thread.NORM_PRIORITY);
+                        newThread.setContextClassLoader(getClass().getClassLoader());
+                        return newThread;
+                    }
+                },
+                (r, executor) -> getServletContext().log("Proxy rejected task: " + r));
+
+        final ClientBuilder clientBuilder = ClientBuilder.newBuilder();
+        clientBuilder.executorService(executor);
+        clientBuilder.readTimeout(getLong("read.timeout").orElse(30000L), MILLISECONDS);
+        clientBuilder.connectTimeout(getLong("connect.timeout").orElse(30000L), MILLISECONDS);
+        // todo: configure ssl
+        // clientBuilder.scheduledExecutorService(); // not used by cxf for instance so no need to overkill the conf
+        client = clientBuilder.build();
+    }
+
+    private Optional<Long> getLong(final String key) {
+        return get(key).map(Long::parseLong);
+    }
+
+    private Optional<Integer> getInt(final String key) {
+        return get(key).map(Integer::parseInt);
+    }
+
+    private Optional<String> get(final String key) {
+        return ofNullable(getServletConfig().getInitParameter(key));
+    }
+}
diff --git a/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/cdi/CDIProxyServlet.java b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/cdi/CDIProxyServlet.java
new file mode 100644
index 0000000..44fb9ef
--- /dev/null
+++ b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/cdi/CDIProxyServlet.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.meecrowave.proxy.servlet.front.cdi;
+
+import java.io.IOException;
+import java.util.concurrent.CompletionStage;
+
+import javax.enterprise.event.Event;
+import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.meecrowave.proxy.servlet.configuration.Routes;
+import org.apache.meecrowave.proxy.servlet.front.ProxyServlet;
+import org.apache.meecrowave.proxy.servlet.front.cdi.event.AfterResponse;
+import org.apache.meecrowave.proxy.servlet.front.cdi.event.BeforeRequest;
+
+// IMPORTANT: don't make this class depending on meecrowave, cxf or our internals, use setup class
+public class CDIProxyServlet extends ProxyServlet {
+    @Inject
+    private Event<BeforeRequest> beforeRequestEvent;
+
+    @Inject
+    private Event<AfterResponse> afterResponseEvent;
+
+    @Override
+    protected CompletionStage<HttpServletResponse> doExecute(final Routes.Route route, final HttpServletRequest req, final HttpServletResponse resp,
+                                                             final String prefix) throws IOException {
+        final BeforeRequest event = new BeforeRequest(req, resp);
+        event.setRoute(route);
+        event.setPrefix(prefix);
+        beforeRequestEvent.fire(event);
+        return super.doExecute(event.getRoute(), req, resp, event.getPrefix())
+            .handle((r, t) -> {
+                afterResponseEvent.fire(new AfterResponse(req, resp));
+                return r;
+            });
+    }
+}
diff --git a/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/cdi/event/AfterResponse.java b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/cdi/event/AfterResponse.java
new file mode 100644
index 0000000..5bf7baf
--- /dev/null
+++ b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/cdi/event/AfterResponse.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.meecrowave.proxy.servlet.front.cdi.event;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.meecrowave.proxy.servlet.configuration.Routes;
+
+public class AfterResponse extends BaseEvent {
+    private Routes.Route route;
+    private String prefix;
+
+    public AfterResponse(final HttpServletRequest request, final HttpServletResponse response) {
+        super(request, response);
+    }
+
+    public String getPrefix() {
+        return prefix;
+    }
+
+    public void setPrefix(final String prefix) {
+        this.prefix = prefix;
+    }
+
+    public Routes.Route getRoute() {
+        return route;
+    }
+
+    public void setRoute(final Routes.Route route) {
+        this.route = route;
+    }
+}
diff --git a/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/cdi/event/BaseEvent.java b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/cdi/event/BaseEvent.java
new file mode 100644
index 0000000..c01b738
--- /dev/null
+++ b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/cdi/event/BaseEvent.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright (C) 2006-2019 Talend Inc. - www.talend.com
+ * <p>
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.apache.meecrowave.proxy.servlet.front.cdi.event;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class BaseEvent {
+    private final HttpServletRequest request;
+    private final HttpServletResponse response;
+
+    protected BaseEvent(final HttpServletRequest request, final HttpServletResponse response) {
+        this.request = request;
+        this.response = response;
+    }
+
+    public HttpServletRequest getRequest() {
+        return request;
+    }
+
+    public HttpServletResponse getResponse() {
+        return response;
+    }
+}
diff --git a/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/cdi/event/BeforeRequest.java b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/cdi/event/BeforeRequest.java
new file mode 100644
index 0000000..43fa33d
--- /dev/null
+++ b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/front/cdi/event/BeforeRequest.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.meecrowave.proxy.servlet.front.cdi.event;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.meecrowave.proxy.servlet.configuration.Routes;
+
+public class BeforeRequest extends BaseEvent {
+    private Routes.Route route;
+    private String prefix;
+
+    public BeforeRequest(final HttpServletRequest request, final HttpServletResponse response) {
+        super(request, response);
+    }
+
+    public String getPrefix() {
+        return prefix;
+    }
+
+    public void setPrefix(final String prefix) {
+        this.prefix = prefix;
+    }
+
+    public Routes.Route getRoute() {
+        return route;
+    }
+
+    public void setRoute(final Routes.Route route) {
+        this.route = route;
+    }
+}
diff --git a/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/meecrowave/ProxyServletSetup.java b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/meecrowave/ProxyServletSetup.java
new file mode 100644
index 0000000..c581c97
--- /dev/null
+++ b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/meecrowave/ProxyServletSetup.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.meecrowave.proxy.servlet.meecrowave;
+
+import javax.servlet.MultipartConfigElement;
+import javax.servlet.ServletRegistration;
+
+import org.apache.catalina.Context;
+import org.apache.meecrowave.Meecrowave;
+import org.apache.meecrowave.proxy.servlet.front.cdi.CDIProxyServlet;
+import org.apache.meecrowave.runner.Cli;
+import org.apache.meecrowave.runner.cli.CliOption;
+
+// IMPORTANT: don't make this class depending on meecrowave, cxf or our internals, use setup class
+public class ProxyServletSetup implements Meecrowave.MeecrowaveAwareContextCustomizer {
+    private Meecrowave instance;
+
+    @Override
+    public void accept(final Context context) {
+        final Configuration config = instance.getConfiguration().getExtension(Configuration.class);
+        if (config.skip) {
+            return;
+        }
+        context.addServletContainerInitializer((c, ctx) -> {
+            final ServletRegistration.Dynamic servlet = ctx.addServlet("meecrowave-proxy-servlet", CDIProxyServlet.class);
+            servlet.setLoadOnStartup(1);
+            servlet.setAsyncSupported(true);
+            servlet.addMapping(config.mapping);
+            if (config.multipart) {
+                servlet.setMultipartConfig(new MultipartConfigElement(
+                        config.multipartLocation,
+                        config.multipartMaxFileSize,
+                        config.multipartMaxRequestSize,
+                        config.multipartFileSizeThreshold));
+            }
+            servlet.setInitParameter("mapping", config.mapping);
+            servlet.setInitParameter("configuration", config.configuration);
+            servlet.setInitParameter("read.timeout", config.readTimeout);
+            servlet.setInitParameter("connect.timeout", config.connectTimeout);
+            servlet.setInitParameter("executor.core", config.threadPoolCoreSize);
+            servlet.setInitParameter("executor.max", config.threadPoolMaxSize);
+            servlet.setInitParameter("executor.keepAlive", config.threadPoolKeepAlive);
+            servlet.setInitParameter("shutdown.timeout", config.shutdownTimeout);
+            servlet.setInitParameter("async.timeout", config.asyncTimeout);
+        }, null);
+    }
+
+    @Override
+    public void setMeecrowave(final Meecrowave meecrowave) {
+        this.instance = meecrowave;
+    }
+
+    public static class Configuration implements Cli.Options {
+        @CliOption(name = "proxy-skip", description = "Should default setup be ignored")
+        private boolean skip = false;
+
+        @CliOption(name = "proxy-mapping", description = "Where to bind the proxy (url pattern).")
+        private String mapping = "/*";
+
+        @CliOption(name = "proxy-multipart", description = "Is multipart explicit.")
+        private boolean multipart = true;
+
+        @CliOption(name = "proxy-multipart-maxfilesize", description = "Max file size for multipart requests.")
+        private long multipartMaxFileSize = -1;
+
+        @CliOption(name = "proxy-multipart-maxrequestsize", description = "Max request size for multipart requests.")
+        private long multipartMaxRequestSize = -1;
+
+        @CliOption(name = "proxy-multipart-maxfilesizethreshold", description = "Max file size threshold for multipart requests.")
+        private int multipartFileSizeThreshold = 0;
+
+        @CliOption(name = "proxy-multipart-location", description = "The multipart temporary folder.")
+        private String multipartLocation = "";
+
+        @CliOption(name = "proxy-configuration", description = "The route file.")
+        private String configuration = "conf/proxy.json";
+
+        @CliOption(name = "proxy-shutdown-timeout", description = "How long the shutdown will wait for in progress tasks (executor).")
+        private String shutdownTimeout = "30000";
+
+        @CliOption(name = "proxy-executor-core", description = "HTTP client thread pool core size.")
+        private String threadPoolCoreSize = "64";
+
+        @CliOption(name = "proxy-executor-max", description = "HTTP client thread pool max size.")
+        private String threadPoolMaxSize = "512";
+
+        @CliOption(name = "proxy-executor-max", description = "HTTP client thread pool keep alive duration (in ms).")
+        private String threadPoolKeepAlive = "60000";
+
+        @CliOption(name = "proxy-read-timeout", description = "HTTP client read timeout.")
+        private String readTimeout = "30000";
+
+        @CliOption(name = "proxy-connect-timeout", description = "HTTP client connect timeout.")
+        private String connectTimeout = "30000";
+
+        @CliOption(name = "proxy-async-timeout", description = "Asynchronous execution timeout.")
+        private String asyncTimeout = "30000";
+    }
+}
diff --git a/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/service/ConfigurationLoader.java b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/service/ConfigurationLoader.java
new file mode 100644
index 0000000..bd3fd53
--- /dev/null
+++ b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/service/ConfigurationLoader.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.meecrowave.proxy.servlet.service;
+
+import static java.util.Collections.singletonList;
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toMap;
+
+import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Optional;
+
+import javax.json.bind.Jsonb;
+import javax.json.bind.JsonbBuilder;
+import javax.json.bind.JsonbConfig;
+import javax.json.spi.JsonProvider;
+
+import org.apache.meecrowave.proxy.servlet.configuration.Routes;
+
+public class ConfigurationLoader {
+    private final String path;
+
+    private Routes routes;
+
+    public ConfigurationLoader(final String path) {
+        this.path = path;
+    }
+
+    public Optional<Routes> load() {
+        final SimpleSubstitutor simpleSubstitutor = new SimpleSubstitutor(
+                System.getProperties().stringPropertyNames().stream().collect(toMap(identity(), System::getProperty)));
+        final Path routeFile = Paths.get(simpleSubstitutor.replace(path));
+        if (!Files.exists(routeFile)) {
+            throw new IllegalArgumentException("No routes configuration for the proxy servlet");
+        }
+
+        try (final InputStream stream = Files.newInputStream(routeFile);
+             final Jsonb jsonb = JsonbBuilder.newBuilder()
+                     .withProvider(loadJsonpProvider())
+                     .withConfig(new JsonbConfig().setProperty("org.apache.johnzon.supports-comments", true))
+                     .build()) {
+            routes = jsonb.fromJson(stream, Routes.class);
+        } catch (final Exception e) {
+            throw new IllegalArgumentException(e);
+        }
+        final boolean hasRoutes = routes.routes != null && !routes.routes.isEmpty();
+        if (routes.defaultRoute == null && !hasRoutes) {
+            return Optional.empty();
+        }
+        if (routes.defaultRoute != null) {
+            onLoad(simpleSubstitutor, routes.defaultRoute);
+            if (routes.routes == null) { // no route were defined, consider it is the default route, /!\ empty means no route, don't default
+                routes.routes = singletonList(routes.defaultRoute);
+            }
+            if (hasRoutes) {
+                routes.routes.forEach(r -> merge(routes.defaultRoute, r));
+            }
+        }
+        if (hasRoutes) {
+            routes.routes.forEach(it -> onLoad(simpleSubstitutor, it));
+        }
+        return Optional.of(routes);
+    }
+
+    private void merge(final Routes.Route defaultRoute, final Routes.Route route) {
+        if (route.requestConfiguration == null) {
+            route.requestConfiguration = defaultRoute.requestConfiguration;
+        } else if (defaultRoute.requestConfiguration != null) {
+            if (route.requestConfiguration.method == null) {
+                route.requestConfiguration.method = defaultRoute.requestConfiguration.method;
+            }
+            if (route.requestConfiguration.prefix == null) {
+                route.requestConfiguration.prefix = defaultRoute.requestConfiguration.prefix;
+            }
+            if (route.requestConfiguration.skippedCookies == null) {
+                route.requestConfiguration.skippedCookies = defaultRoute.requestConfiguration.skippedCookies;
+            }
+            if (route.requestConfiguration.skippedHeaders == null) {
+                route.requestConfiguration.skippedHeaders = defaultRoute.requestConfiguration.skippedHeaders;
+            }
+        }
+
+        if (route.responseConfiguration == null) {
+            route.responseConfiguration = defaultRoute.responseConfiguration;
+        } else if (defaultRoute.responseConfiguration != null) {
+            if (route.responseConfiguration.target == null) {
+                route.responseConfiguration.target = defaultRoute.responseConfiguration.target;
+            }
+            if (route.responseConfiguration.skippedCookies == null) {
+                route.responseConfiguration.skippedCookies = defaultRoute.responseConfiguration.skippedCookies;
+            }
+            if (route.responseConfiguration.skippedHeaders == null) {
+                route.responseConfiguration.skippedHeaders = defaultRoute.responseConfiguration.skippedHeaders;
+            }
+        }
+    }
+
+    private JsonProvider loadJsonpProvider() {
+        try { // prefer johnzon to support comments
+            return JsonProvider.class.cast(Thread.currentThread().getContextClassLoader()
+                    .loadClass("org.apache.johnzon.core.JsonProviderImpl")
+                    .getConstructor().newInstance());
+        } catch (final InvocationTargetException | NoClassDefFoundError | InstantiationException | ClassNotFoundException | NoSuchMethodException | IllegalAccessException err) {
+            return JsonProvider.provider();
+        }
+    }
+
+    // filter
+    private void onLoad(final SimpleSubstitutor simpleSubstitutor, final Routes.Route route) {
+        if (route.requestConfiguration != null && route.requestConfiguration.prefix != null) {
+            route.requestConfiguration.prefix = simpleSubstitutor.replace(route.requestConfiguration.prefix);
+        }
+        if (route.requestConfiguration != null && route.requestConfiguration.method != null) {
+            route.requestConfiguration.method = simpleSubstitutor.replace(route.requestConfiguration.method);
+        }
+        if (route.responseConfiguration != null && route.responseConfiguration.target != null) {
+            route.responseConfiguration.target = simpleSubstitutor.replace(route.responseConfiguration.target);
+        }
+    }
+}
diff --git a/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/service/SimpleSubstitutor.java b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/service/SimpleSubstitutor.java
new file mode 100644
index 0000000..4bda989
--- /dev/null
+++ b/meecrowave-proxy/src/main/java/org/apache/meecrowave/proxy/servlet/service/SimpleSubstitutor.java
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.meecrowave.proxy.servlet.service;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+// duplicated to not depend on core if this module is deployed in another servlet container
+public class SimpleSubstitutor {
+    private final char[] prefix = "${".toCharArray();
+    private final char[] suffix = "}".toCharArray();
+    private final char[] valueDelimiter = ":-".toCharArray();
+    private final Map<String, String> valueMap;
+
+    public SimpleSubstitutor(final Map<String, String> valueMap) {
+        this.valueMap = valueMap;
+    }
+
+    public String replace(final String source) {
+        if (source == null) {
+            return null;
+        }
+        final StringBuilder builder = new StringBuilder(source);
+        if (substitute(builder, 0, source.length(), null) <= 0) {
+            return source;
+        }
+        return replace(builder.toString());
+    }
+
+    private int substitute(final StringBuilder buf, final int offset, final int length, List<String> priorVariables) {
+        final boolean top = priorVariables == null;
+        boolean altered = false;
+        int lengthChange = 0;
+        char[] chars = buf.toString().toCharArray();
+        int bufEnd = offset + length;
+        int pos = offset;
+        while (pos < bufEnd) {
+            final int startMatchLen = isMatch(prefix, chars, pos, bufEnd);
+            if (startMatchLen == 0) {
+                pos++;
+            } else {
+                if (pos > offset && chars[pos - 1] == '$') {
+                    buf.deleteCharAt(pos - 1);
+                    chars = buf.toString().toCharArray();
+                    lengthChange--;
+                    altered = true;
+                    bufEnd--;
+                } else {
+                    final int startPos = pos;
+                    pos += startMatchLen;
+                    int endMatchLen;
+                    while (pos < bufEnd) {
+                        endMatchLen = isMatch(suffix, chars, pos, bufEnd);
+                        if (endMatchLen == 0) {
+                            pos++;
+                        } else {
+                            String varNameExpr = new String(chars, startPos
+                                    + startMatchLen, pos - startPos
+                                    - startMatchLen);
+                            pos += endMatchLen;
+                            final int endPos = pos;
+
+                            String varName = varNameExpr;
+                            String varDefaultValue = null;
+
+                            final char[] varNameExprChars = varNameExpr.toCharArray();
+                            for (int i = 0; i < varNameExprChars.length; i++) {
+                                if (isMatch(prefix, varNameExprChars, i, varNameExprChars.length) != 0) {
+                                    break;
+                                }
+                                final int match = isMatch(valueDelimiter, varNameExprChars, i, varNameExprChars.length);
+                                if (match != 0) {
+                                    varName = varNameExpr.substring(0, i);
+                                    varDefaultValue = varNameExpr.substring(i + match);
+                                    break;
+                                }
+                            }
+
+                            if (priorVariables == null) {
+                                priorVariables = new ArrayList<>();
+                                priorVariables.add(new String(chars,
+                                        offset, length));
+                            }
+
+                            checkCyclicSubstitution(varName, priorVariables);
+                            priorVariables.add(varName);
+
+                            final String varValue = getOrDefault(varName, varDefaultValue);
+                            if (varValue != null) {
+                                final int varLen = varValue.length();
+                                buf.replace(startPos, endPos, varValue);
+                                altered = true;
+                                int change = substitute(buf, startPos, varLen, priorVariables);
+                                change = change + varLen - (endPos - startPos);
+                                pos += change;
+                                bufEnd += change;
+                                lengthChange += change;
+                                chars = buf.toString().toCharArray();
+                            }
+
+                            priorVariables.remove(priorVariables.size() - 1);
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+        if (top) {
+            return altered ? 1 : 0;
+        }
+        return lengthChange;
+    }
+
+    protected String getOrDefault(final String varName, final String varDefaultValue) {
+        return valueMap.getOrDefault(varName, varDefaultValue);
+    }
+
+    private int isMatch(final char[] chars, final char[] buffer, int pos,
+                        final int bufferEnd) {
+        final int len = chars.length;
+        if (pos + len > bufferEnd) {
+            return 0;
+        }
+        for (int i = 0; i < chars.length; i++, pos++) {
+            if (chars[i] != buffer[pos]) {
+                return 0;
+            }
+        }
+        return len;
+    }
+
+    private void checkCyclicSubstitution(final String varName, final List<String> priorVariables) {
+        if (!priorVariables.contains(varName)) {
+            return;
+        }
+        final StringBuilder buf = new StringBuilder(256);
+        buf.append("Infinite loop in property interpolation of ");
+        buf.append(priorVariables.remove(0));
+        buf.append(": ");
+        appendWithSeparators(buf, priorVariables);
+        throw new IllegalStateException(buf.toString());
+    }
+
+    private void appendWithSeparators(final StringBuilder builder, final Collection<String> iterable) {
+        if (iterable != null && !iterable.isEmpty()) {
+            final Iterator<?> it = iterable.iterator();
+            while (it.hasNext()) {
+                builder.append(it.next());
+                if (it.hasNext()) {
+                    builder.append("->");
+                }
+            }
+        }
+    }
+}
diff --git a/meecrowave-proxy/src/main/resources/META-INF/beans.xml b/meecrowave-proxy/src/main/resources/META-INF/beans.xml
new file mode 100644
index 0000000..e250b87
--- /dev/null
+++ b/meecrowave-proxy/src/main/resources/META-INF/beans.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you 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.
+-->
+<beans bean-discovery-mode="all" version="2.0"
+       xmlns="http://xmlns.jcp.org/xml/ns/javaee"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="
+        http://xmlns.jcp.org/xml/ns/javaee
+        http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd">
+
+  <trim/>
+</beans>
diff --git a/meecrowave-proxy/src/main/resources/META-INF/services/org.apache.meecrowave.Meecrowave$ContextCustomizer b/meecrowave-proxy/src/main/resources/META-INF/services/org.apache.meecrowave.Meecrowave$ContextCustomizer
new file mode 100644
index 0000000..f8a9cf2
--- /dev/null
+++ b/meecrowave-proxy/src/main/resources/META-INF/services/org.apache.meecrowave.Meecrowave$ContextCustomizer
@@ -0,0 +1 @@
+org.apache.meecrowave.proxy.servlet.meecrowave.ProxyServletSetup
diff --git a/meecrowave-proxy/src/test/java/org/apache/meecrowave/proxy/servlet/ProxyServletTest.java b/meecrowave-proxy/src/test/java/org/apache/meecrowave/proxy/servlet/ProxyServletTest.java
new file mode 100644
index 0000000..6b9bab3
--- /dev/null
+++ b/meecrowave-proxy/src/test/java/org/apache/meecrowave/proxy/servlet/ProxyServletTest.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.meecrowave.proxy.servlet;
+
+import static java.util.Optional.ofNullable;
+import static javax.ws.rs.client.Entity.entity;
+import static javax.ws.rs.core.MediaType.TEXT_PLAIN_TYPE;
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.nio.charset.StandardCharsets;
+import java.util.function.Consumer;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Response;
+
+import org.apache.meecrowave.Meecrowave;
+import org.apache.meecrowave.io.IO;
+import org.apache.meecrowave.junit.MeecrowaveRule;
+import org.apache.meecrowave.proxy.servlet.mock.FakeRemoteServer;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+
+public class ProxyServletTest {
+    @ClassRule(order = 1)
+    public static final TestRule FAKE_REMOTE_SERVER = new FakeRemoteServer()
+            .with(server -> server.createContext("/simple", exchange -> {
+                final byte[] out = ("{\"message\":\"" + ofNullable(exchange.getRequestBody()).map(it -> {
+                    try {
+                        return IO.toString(it);
+                    } catch (final IOException e) {
+                        throw new IllegalStateException(e);
+                    }
+                }).orElse("ok") + "\"}").getBytes(StandardCharsets.UTF_8);
+                exchange.getResponseHeaders().add("Fake-Server", "true");
+                exchange.getResponseHeaders().add("Foo", ofNullable(exchange.getRequestURI().getQuery()).orElse("-"));
+                exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, out.length);
+                try (final OutputStream os = exchange.getResponseBody()) {
+                    os.write(out);
+                }
+            }))
+            .with(server -> server.createContext("/data1", exchange -> {
+                final byte[] out = ("{\"message\":\"" + IO.toString(exchange.getRequestBody()) + "\"}")
+                        .getBytes(StandardCharsets.UTF_8);
+                exchange.getResponseHeaders().add("Fake-Server", "posted");
+                exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, out.length);
+                try (final OutputStream os = exchange.getResponseBody()) {
+                    os.write(out);
+                }
+            }));
+
+    @ClassRule(order = 2)
+    public static final MeecrowaveRule MW = new MeecrowaveRule(new Meecrowave.Builder()
+            .property("proxy-configuration", "target/test-classes/routes.json"), "");
+
+    @Test
+    public void get() {
+        withClient(target -> {
+            final Response response = target.path("/simple").request().get();
+            assertEquals(HttpURLConnection.HTTP_OK, response.getStatus());
+            assertEquals("true", response.getHeaderString("Fake-Server"));
+            assertEquals("{\"message\":\"\"}", response.readEntity(String.class));
+        });
+    }
+
+    @Test
+    public void getWithQuery() {
+        withClient(target -> {
+            final Response response = target.path("/simple").queryParam("foo", "bar").request().get();
+            assertEquals(HttpURLConnection.HTTP_OK, response.getStatus());
+            assertEquals("foo=bar", response.getHeaderString("Foo"));
+            assertEquals("{\"message\":\"\"}", response.readEntity(String.class));
+        });
+    }
+
+    @Test
+    public void post() {
+        withClient(target -> {
+            final Response response = target.path("/data1").request().post(entity("data were sent", TEXT_PLAIN_TYPE));
+            assertEquals(HttpURLConnection.HTTP_OK, response.getStatus());
+            assertEquals("posted", response.getHeaderString("Fake-Server"));
+            assertEquals("{\"message\":\"data were sent\"}", response.readEntity(String.class));
+        });
+    }
+
+    private void withClient(final Consumer<WebTarget> withBase) {
+        final Client client = ClientBuilder.newClient();
+        try {
+            withBase.accept(client.target("http://localhost:" + MW.getConfiguration().getHttpPort()));
+        } finally {
+            client.close();
+        }
+    }
+}
diff --git a/meecrowave-proxy/src/test/java/org/apache/meecrowave/proxy/servlet/mock/FakeRemoteServer.java b/meecrowave-proxy/src/test/java/org/apache/meecrowave/proxy/servlet/mock/FakeRemoteServer.java
new file mode 100644
index 0000000..8f18c6e
--- /dev/null
+++ b/meecrowave-proxy/src/test/java/org/apache/meecrowave/proxy/servlet/mock/FakeRemoteServer.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.meecrowave.proxy.servlet.mock;
+
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.function.Consumer;
+
+import com.sun.net.httpserver.HttpServer;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+public class FakeRemoteServer implements TestRule {
+    private HttpServer server;
+    private final Collection<Consumer<HttpServer>> configurers = new ArrayList<>();
+
+    public HttpServer getServer() {
+        return server;
+    }
+
+    public FakeRemoteServer with(final Consumer<HttpServer> configurer) {
+        configurers.add(configurer);
+        return this;
+    }
+
+    @Override
+    public Statement apply(final Statement statement, final Description description) {
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                try {
+                    server = HttpServer.create(new InetSocketAddress(0), 0);
+                    configurers.forEach(it -> it.accept(server));
+                    server.start();
+                    System.setProperty("fake.server.port", Integer.toString(server.getAddress().getPort()));
+                    statement.evaluate();
+                } finally {
+                    server.stop(0);
+                    server = null;
+                    System.clearProperty("fake.server.port");
+                }
+            }
+        };
+    }
+}
diff --git a/meecrowave-proxy/src/test/resources/routes.json b/meecrowave-proxy/src/test/resources/routes.json
new file mode 100644
index 0000000..13a9a5d
--- /dev/null
+++ b/meecrowave-proxy/src/test/resources/routes.json
@@ -0,0 +1,28 @@
+{
+  "defaultRoute": {
+    "responseConfiguration": {
+      // configure our global fake server for all the endpoints
+      "target": "http://localhost:${fake.server.port}"
+    }
+  },
+  "routes": [
+    {
+      // used to test a plain simple static endpoint - simplest case
+      "id": "get-simple",
+      "requestConfiguration": {
+        "method": "GET",
+        "prefix": "/simple"
+      }
+    },
+    {
+      /**
+        * used to test a very trivial post without any query param or anything else
+        */
+      "id": "post-simple",
+      "requestConfiguration": {
+        "method": "POST",
+        "prefix": "/data1"
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index ac4c16b..e276109 100644
--- a/pom.xml
+++ b/pom.xml
@@ -49,7 +49,7 @@
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     <meecrowave.build.name>${project.groupId}.${project.artifactId}</meecrowave.build.name>
 
-    <junit.version>4.12</junit.version>
+    <junit.version>4.13-beta-3</junit.version>
     <tomcat.version>9.0.20</tomcat.version>
     <openwebbeans.version>2.0.11</openwebbeans.version>
     <cxf.version>3.3.2</cxf.version>
@@ -81,6 +81,7 @@
     <module>integration-tests</module>
     <module>meecrowave-oauth2</module>
     <module>meecrowave-letsencrypt</module>
+    <module>meecrowave-proxy</module>
   </modules>
 
   <dependencyManagement>