KEYCLOAK-7412 Tests for Fuse 7.0

This commit is contained in:
Hynek Mlnarik 2018-06-13 14:22:35 +02:00 committed by Pavel Drozd
parent f5ca4840d6
commit 530a710dce
21 changed files with 267 additions and 90 deletions

View file

@ -282,5 +282,13 @@
"client": "hawtio-client",
"roles": [ "viewer", "jmxAdmin" ]
}
]
],
"clientScopeMappings": {
"account": [
{
"client": "hawtio-client",
"roles": [ "view-profile", "manage-account" ]
}
]
}
}

View file

@ -109,14 +109,14 @@ Assumed you downloaded `jboss-fuse-karaf-6.3.0.redhat-229.zip`
# Prepare Fuse server
mvn -f testsuite/integration-arquillian/servers \
mvn -f testsuite/integration-arquillian/servers/pom.xml \
clean install \
-Pauth-server-wildfly \
-Papp-server-fuse63 \
-Dfuse63.version=6.3.0.redhat-229 \
-Dapp.server.karaf.update.config=true \
-Dmaven.local.settings=$HOME/.m2/settings.xml \
-Drepositories=,http://download.eng.bos.redhat.com/brewroot/repos/sso-7.1-build/latest/maven/ \
-Drepositories=,http://REPO-SERVER/brewroot/repos/sso-7.1-build/latest/maven/ \
-Dmaven.repo.local=$HOME/.m2/repository
# Run the Fuse adapter tests
@ -127,6 +127,42 @@ Assumed you downloaded `jboss-fuse-karaf-6.3.0.redhat-229.zip`
-Dfuse63.version=6.3.0.redhat-229
### JBoss Fuse 7.0
1) Download JBoss Fuse 7.0 to your filesystem. It can be downloaded from http://origin-repository.jboss.org/nexus/content/groups/m2-proxy/org/jboss/fuse/fuse-karaf
Assumed you downloaded `fuse-karaf-7.0.0.fuse-000202.zip`
2) Install to your local maven repository and change the properties according to your env (This step can be likely avoided if you somehow configure your local maven settings to point directly to Fuse repo):
mvn install:install-file \
-DgroupId=org.jboss.fuse \
-DartifactId=fuse-karaf \
-Dversion=7.0.0.fuse-000202 \
-Dpackaging=zip \
-Dfile=/mydownloads/fuse-karaf-7.0.0.fuse-000202.zip
3) Prepare Fuse and run the tests (change props according to your environment, versions etc):
# Prepare Fuse server
mvn -f testsuite/integration-arquillian/servers/pom.xml \
clean install \
-Papp-server-fuse70 \
-Dfuse70.version=7.0.0.fuse-000202 \
-Dapp.server.karaf.update.config=true \
-Dmaven.local.settings=$HOME/.m2/settings.xml \
-Drepositories=,http://REPO-SERVER/brewroot/repos/sso-7.1-build/latest/maven/ \
-Dmaven.repo.local=$HOME/.m2/repository
# Run the Fuse adapter tests
mvn -f testsuite/integration-arquillian/tests/other/adapters/karaf/fuse70/pom.xml \
clean test \
-Dbrowser=phantomjs \
-Papp-server-fuse70
### EAP6 with Hawtio
1) Download JBoss EAP 6.4.0.GA zip

View file

@ -1,3 +1,7 @@
feature:repo-add mvn:org.keycloak/keycloak-osgi-features/${project.version}/xml/features
feature:repo-add mvn:org.keycloak.example.demo/keycloak-fuse-example-features/${project.version}/xml/features
feature:install pax-http-undertow
feature:install keycloak-jaas keycloak-pax-http-undertow
feature:install keycloak-fuse-7.0-example

View file

@ -0,0 +1,9 @@
{
"realm" : "demo",
"resource" : "jaas",
"bearer-only" : true,
"auth-server-url" : "http://localhost:8080/auth",
"ssl-required" : "external",
"use-resource-role-mappings": false,
"principal-attribute": "preferred_username"
}

View file

@ -1,9 +1,9 @@
{
"realm": "demo",
"resource": "ssh-jmx-admin-client",
"ssl-required" : "external",
"auth-server-url" : "http://localhost:8080/auth",
"credentials": {
"secret": "password"
}
"realm" : "demo",
"resource" : "ssh-jmx-admin-client",
"auth-server-url" : "http://localhost:8080/auth",
"ssl-required" : "external",
"credentials": {
"secret": "password"
}
}

View file

@ -1,7 +1,7 @@
{
"realm" : "demo",
"resource" : "hawtio-client",
"auth-server-url" : "http://localhost:8080/auth",
"clientId" : "hawtio-client",
"url" : "http://localhost:8080/auth",
"ssl-required" : "external",
"public-client" : true
}

View file

@ -1,9 +1,9 @@
{
"realm" : "demo",
"resource" : "jaas",
"bearer-only" : true,
"resource" : "ssh-jmx-admin-client",
"auth-server-url" : "http://localhost:8080/auth",
"ssl-required" : "external",
"use-resource-role-mappings": false,
"principal-attribute": "preferred_username"
"credentials": {
"secret": "password"
}
}

View file

@ -1,8 +1,9 @@
config:edit org.apache.karaf.shell
config:property-set sshRealm keycloak
config:update
system:property -p hawtio.roles admin,user
system:property -p hawtio.keycloakEnabled true
system:property -p hawtio.realm keycloak
system:property -p hawtio.keycloakClientConfig file://\$\{karaf.base\}/etc/keycloak-hawtio-client.json
system:property -p hawtio.keycloakClientConfig file://\${karaf.base}/etc/keycloak-hawtio-client.json
system:property -p hawtio.keycloakServerConfig file://\${karaf.base}/etc/keycloak-bearer.json
system:property -p hawtio.rolePrincipalClasses org.keycloak.adapters.jaas.RolePrincipal,org.apache.karaf.jaas.boot.principal.RolePrincipal

View file

@ -3,6 +3,11 @@ config:property-set org.ops4j.pax.url.mvn.localRepository ${maven.repo.local}
config:property-set org.ops4j.pax.url.mvn.settings ${maven.local.settings}
config:property-append org.ops4j.pax.url.mvn.repositories ${repositories}
config:update
config:edit org.ops4j.pax.web
config:property-set org.ops4j.pax.web.config.file '${karaf.etc}/undertow.xml'
config:update
config:edit jmx.acl.org.apache.karaf.security.jmx
config:property-append list* viewer
config:property-append set* jmxAdmin

View file

@ -138,6 +138,7 @@
<directory>src/main/resources</directory>
<includes>
<include>users.properties</include>
<include>keycloak-bearer.json</include>
<include>keycloak-direct-access.json</include>
<include>keycloak-hawtio-client.json</include>
<include>keycloak-hawtio.json</include>

View file

@ -68,13 +68,17 @@ class SimpleWebXmlParser {
List<ElementWrapper> servlets = document.getElementsByTagName("servlet");
for (ElementWrapper servlet : servlets) {
String servletName = servlet.getElementByTagName("servlet-name").getText();
String servletClass = servlet.getElementByTagName("servlet-class").getText();
ElementWrapper servletClassEw = servlet.getElementByTagName("servlet-class");
String servletClass = servletClassEw == null ? servletName : servletClassEw.getText();
ElementWrapper loadOnStartupEw = servlet.getElementByTagName("load-on-startup");
Integer loadOnStartup = loadOnStartupEw == null ? null : Integer.valueOf(loadOnStartupEw.getText());
Class<? extends Servlet> servletClazz = (Class<? extends Servlet>) Class.forName(servletClass);
ServletInfo undertowServlet = new ServletInfo(servletName, servletClazz);
if (servletMappings.containsKey(servletName)) {
undertowServlet.addMapping(servletMappings.get(servletName));
undertowServlet.setLoadOnStartup(loadOnStartup);
di.addServlet(undertowServlet);
} else {
log.warnf("Missing servlet-mapping for '%s'", servletName);

View file

@ -0,0 +1,41 @@
package org.keycloak.testsuite.adapter.page;
import org.keycloak.testsuite.page.AbstractPage;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import javax.ws.rs.core.UriBuilder;
import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
/**
* @author mhajas
*/
public class Hawtio2Page extends AbstractPage {
public String getUrl() {
if (Boolean.parseBoolean(System.getProperty("app.server.ssl.required"))) {
return "https://localhost:" + System.getProperty("app.server.https.port", "8543") + "/hawtio";
}
return "http://localhost:" + System.getProperty("app.server.http.port", "8180") + "/hawtio";
}
@Override
public UriBuilder createUriBuilder() {
return UriBuilder.fromUri(getUrl());
}
@FindBy(xpath = "//a[@id ='userDropdownMenu']")
private WebElement dropDownMenu;
@FindBy(xpath = "//a[@ng-click='userDetails.logout()']")
private WebElement logoutButton;
public void logout() {
waitUntilElement(dropDownMenu).is().visible();
dropDownMenu.click();
waitUntilElement(logoutButton).is().visible();
logoutButton.click();
}
}

View file

@ -148,7 +148,7 @@ public class CustomKarafContainer<T extends KarafManagedContainerConfiguration>
// Get the MBeanServerConnection
try {
mbeanServer = getMBeanServerConnection(30, TimeUnit.SECONDS);
mbeanServer = getMBeanServerConnection(60, TimeUnit.SECONDS);
} catch (Exception ex) {
destroyKarafProcess();
throw new LifecycleException("Cannot obtain MBean server connection", ex);

View file

@ -127,7 +127,7 @@ public class IOUtil {
}
Node node = nodes.item(0).getAttributes().getNamedItem(attributeName);
if (node == null) {
if (node == null || node.getTextContent() == null) {
log.warn("Not able to find attribute " + attributeName + " within element: " + tagName);
return;
}

View file

@ -31,7 +31,6 @@ import javax.management.remote.JMXServiceURL;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.channel.ClientChannel;
import org.apache.sshd.client.channel.ClientChannelEvent;
import org.apache.sshd.client.future.ConnectFuture;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.client.session.ClientSession.ClientSessionEvent;
@ -41,6 +40,14 @@ import org.junit.Test;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.adapter.AbstractExampleAdapterTest;
import org.keycloak.testsuite.adapter.page.HawtioPage;
import org.apache.sshd.client.channel.ChannelExec;
import org.apache.sshd.client.channel.ClientChannel.Streaming;
import org.apache.sshd.client.channel.ClientChannelEvent;
import org.hamcrest.Matchers;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertThat;
public abstract class AbstractFuseAdminAdapterTest extends AbstractExampleAdapterTest {
@ -49,11 +56,7 @@ public abstract class AbstractFuseAdminAdapterTest extends AbstractExampleAdapte
private SshClient client;
private ClientChannel channel;
private ClientSession session;
enum Result { OK, NOT_FOUND, NO_CREDENTIALS, NO_ROLES };
protected enum Result { OK, NOT_FOUND, NO_CREDENTIALS, NO_ROLES };
@Override
public void addAdapterTestRealms(List<RealmRepresentation> testRealms) {
@ -70,6 +73,7 @@ public abstract class AbstractFuseAdminAdapterTest extends AbstractExampleAdapte
@Test
public void hawtioLoginTest() throws Exception {
// Note that this does works only in Fuse 6 with Hawtio 1 since Fuse 7 contains Hawtio 2, and is thus overriden in Fuse 7 test classes
hawtioPage.navigateTo();
testRealmLoginPage.form().login("user", "invalid-password");
assertCurrentUrlDoesntStartWith(hawtioPage);
@ -84,14 +88,15 @@ public abstract class AbstractFuseAdminAdapterTest extends AbstractExampleAdapte
hawtioPage.navigateTo();
testRealmLoginPage.form().login("mary", "password");
assertTrue(!driver.getPageSource().contains("welcome"));
assertThat(driver.getPageSource(), not(containsString("welcome")));
}
@Test
public void sshLoginTest() throws Exception {
assertCommand("mary", "password", "shell:date", Result.NOT_FOUND);
// Note that this does not work for Fuse 7 since the error codes have changed, and is thus overriden for Fuse 7 test classes
assertCommand("mary", "password", "shell:date", Result.NO_CREDENTIALS);
assertCommand("john", "password", "shell:info", Result.NO_CREDENTIALS);
assertCommand("john", "password", "shell:date", Result.OK);
assertCommand("root", "password", "shell:info", Result.OK);
@ -121,47 +126,58 @@ public abstract class AbstractFuseAdminAdapterTest extends AbstractExampleAdapte
setJMXAuthentication("karaf", "admin");
}
private String assertCommand(String user, String password, String command, Result result) throws Exception, IOException {
protected String assertCommand(String user, String password, String command, Result result) throws Exception, IOException {
if (!command.endsWith("\n"))
command += "\n";
ByteArrayOutputStream out = new ByteArrayOutputStream();
OutputStream pipe = openSshChannel(user, password, out, out);
pipe.write(command.getBytes());
pipe.flush();
closeSshChannel(pipe);
String output = new String(out.toByteArray());
String output = getCommandOutput(user, password, command);
switch(result) {
case OK:
Assert.assertFalse("Should not contain 'Insufficient credentials' or 'Command not found': " + output,
output.contains("Insufficient credentials") || output.contains("Command not found"));
Assert.assertThat(output,
not(anyOf(containsString("Insufficient credentials"), Matchers.containsString("Command not found"))));
break;
case NOT_FOUND:
Assert.assertTrue("Should contain 'Command not found': " + output,
output.contains("Command not found"));
Assert.assertThat(output,
containsString("Command not found"));
break;
case NO_CREDENTIALS:
Assert.assertTrue("Should contain 'Insufficient credentials': " + output,
output.contains("Insufficient credentials"));
Assert.assertThat(output,
containsString("Insufficient credentials"));
break;
case NO_ROLES:
Assert.assertTrue("Should contain 'Current user has no associated roles': " + output,
output.contains("Current user has no associated roles"));
Assert.assertThat(output,
containsString("Current user has no associated roles"));
break;
default:
Assert.fail("Unexpected enum value: " + result);
}
return output;
}
private OutputStream openSshChannel(String username, String password, OutputStream ... outputs) throws Exception {
protected String getCommandOutput(String user, String password, String command) throws Exception, IOException {
if (!command.endsWith("\n"))
command += "\n";
try (ClientSession session = openSshChannel(user, password);
ChannelExec channel = session.createExecChannel(command);
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
channel.setOut(out);
channel.setErr(out);
channel.open();
channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED, ClientChannelEvent.EOF), 0);
return new String(out.toByteArray());
}
}
protected ClientSession openSshChannel(String username, String password) throws Exception {
client = SshClient.setUpDefaultClient();
client.start();
ConnectFuture future = client.connect(username, "localhost", 8101);
future.await();
session = future.getSession();
ClientSession session = future.getSession();
Set<ClientSessionEvent> ret = EnumSet.of(ClientSessionEvent.WAIT_AUTH);
while (ret.contains(ClientSessionEvent.WAIT_AUTH)) {
@ -172,48 +188,15 @@ public abstract class AbstractFuseAdminAdapterTest extends AbstractExampleAdapte
if (ret.contains(ClientSessionEvent.CLOSED)) {
throw new Exception("Could not open SSH channel");
}
channel = session.createChannel("shell");
PipedOutputStream pipe = new PipedOutputStream();
channel.setIn(new PipedInputStream(pipe));
OutputStream out;
if (outputs.length >= 1) {
out = outputs[0];
} else {
out = new ByteArrayOutputStream();
}
channel.setOut(out);
OutputStream err;
if (outputs.length >= 2) {
err = outputs[1];
} else {
err = new ByteArrayOutputStream();
}
channel.setErr(err);
channel.open();
return pipe;
return session;
}
private void setJMXAuthentication(String realm, String password) throws Exception {
protected void setJMXAuthentication(String realm, String password) throws Exception {
assertCommand("admin", "password", "config:edit org.apache.karaf.management; config:propset jmxRealm " + realm + "; config:update", Result.OK);
getMBeanServerConnection(10000, TimeUnit.MILLISECONDS, "admin", password);
}
private void closeSshChannel(OutputStream pipe) throws IOException {
pipe.write("logout\n".getBytes());
pipe.flush();
channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), TimeUnit.SECONDS.toMillis(15L));
session.close(true);
client.stop();
client = null;
channel = null;
session = null;
}
private Object assertJmxInvoke(boolean expectSuccess, MBeanServerConnection connection, ObjectName mbean, String method,
Object[] params, String[] signature) throws InstanceNotFoundException, MBeanException, ReflectionException, IOException {
try {

View file

@ -27,16 +27,19 @@ import org.keycloak.testsuite.adapter.page.fuse.CustomerPortalFuseExample;
import org.keycloak.testsuite.adapter.page.fuse.ProductPortalFuseExample;
import org.keycloak.testsuite.auth.page.account.Account;
import org.keycloak.testsuite.util.WaitUtils;
import java.io.File;
import java.util.List;
import org.hamcrest.Matchers;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO;
import static org.keycloak.testsuite.util.IOUtil.loadRealm;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWithLoginUrlOf;
import static org.keycloak.testsuite.util.WaitUtils.pause;
/**
*
@ -111,30 +114,36 @@ public abstract class AbstractFuseExampleAdapterTest extends AbstractExampleAdap
assertCurrentUrlStartsWith(customerPortal);
customerPortal.clickAdminInterfaceLink();
WaitUtils.waitForPageToLoad();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
testRealmLoginPage.form().login("admin", "password");
assertCurrentUrlStartsWith(adminInterface);
assertTrue(driver.getPageSource().contains("Hello admin!"));
assertTrue(driver.getPageSource().contains("This second sentence is returned from a Camel RestDSL endpoint"));
assertThat(driver.getPageSource(), containsString("Hello admin!"));
assertThat(driver.getPageSource(), containsString("This second sentence is returned from a Camel RestDSL endpoint"));
customerListing.navigateTo();
WaitUtils.waitForPageToLoad();
customerListing.clickLogOut();
pause(500);
assertCurrentUrlStartsWith(customerPortal);
WaitUtils.waitForPageToLoad();
WaitUtils.pause(2500);
customerPortal.navigateTo();//needed for phantomjs
WaitUtils.waitForPageToLoad();
customerPortal.clickAdminInterfaceLink();
WaitUtils.waitForPageToLoad();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
testRealmLoginPage.form().login("bburke@redhat.com", "password");
assertCurrentUrlStartsWith(adminInterface);
assertTrue(driver.getPageSource().contains("Status code is 403"));
assertThat(driver.getPageSource(), containsString("Status code is 403"));
}
@Test
public void testProductPortal() {
productPortal.navigateTo();
WaitUtils.waitForPageToLoad();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
testRealmLoginPage.form().login("bburke@redhat.com", "password");
@ -145,6 +154,7 @@ public abstract class AbstractFuseExampleAdapterTest extends AbstractExampleAdap
assertTrue(productPortal.getProduct2SecuredText().contains("Product received: id=2"));
productPortal.clickLogOutLink();
WaitUtils.waitForPageToLoad();
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
}

View file

@ -37,6 +37,7 @@
<property name="karafHome">${app.server.home}</property>
<property name="javaHome">${app.server.java.home}</property>
<property name="javaVmArguments">
${app.server.karaf.jvm.debug.args}
${adapter.test.props}
</property>
<property name="jmxServiceURL">service:jmx:rmi://127.0.0.1:44444/jndi/rmi://127.0.0.1:1099/karaf-root</property>

View file

@ -17,9 +17,79 @@
package org.keycloak.testsuite.adapter.example;
import org.keycloak.testsuite.adapter.page.Hawtio2Page;
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
import org.keycloak.testsuite.util.WaitUtils;
import java.util.Arrays;
import java.util.List;
import org.hamcrest.Matchers;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Test;
import org.openqa.selenium.By;
import static org.junit.Assert.assertThat;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlDoesntStartWith;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
@AppServerContainer("app-server-fuse70")
public class Fuse70AdminAdapterTest extends AbstractFuseAdminAdapterTest {
@Page
protected Hawtio2Page hawtioPage;
@Test
@Override
public void hawtioLoginTest() throws Exception {
hawtioPage.navigateTo();
WaitUtils.waitForPageToLoad();
testRealmLoginPage.form().login("user", "invalid-password");
assertCurrentUrlDoesntStartWith(hawtioPage);
testRealmLoginPage.form().login("invalid-user", "password");
assertCurrentUrlDoesntStartWith(hawtioPage);
testRealmLoginPage.form().login("root", "password");
assertCurrentUrlStartsWith(hawtioPage.toString(), hawtioPage.getDriver());
WaitUtils.waitForPageToLoad();
WaitUtils.waitUntilElement(By.linkText("Camel"));
hawtioPage.logout();
WaitUtils.waitForPageToLoad();
assertCurrentUrlStartsWith(testRealmLoginPage);
hawtioPage.navigateTo();
WaitUtils.waitForPageToLoad();
testRealmLoginPage.form().login("mary", "password");
assertCurrentUrlStartsWith(hawtioPage.toString(), hawtioPage.getDriver());
WaitUtils.waitForPageToLoad();
WaitUtils.waitUntilElementIsNotPresent(By.linkText("Camel"));
}
@Test
@Override
public void sshLoginTest() throws Exception {
assertCommand("mary", "password", "shell:date", Result.NOT_FOUND);
assertCommand("john", "password", "shell:info", Result.NOT_FOUND);
assertCommand("john", "password", "shell:date", Result.OK);
assertRoles("root",
"ssh",
"jmxAdmin",
"admin",
"manager",
"viewer",
"Administrator",
"Auditor",
"Deployer",
"Maintainer",
"Operator",
"SuperUser"
);
}
private void assertRoles(String username, String... expectedRoles) throws Exception {
final String commandOutput = getCommandOutput(username, "password", "jaas:whoami -r --no-format");
final List<String> parsedOutput = Arrays.asList(commandOutput.split("\\n+"));
assertThat(parsedOutput, Matchers.containsInAnyOrder(expectedRoles));
}
}

View file

@ -43,6 +43,7 @@
<!--fuse examples expect default karaf http port 8181-->
<app.server.http.port>8181</app.server.http.port>
<app.server.karaf.jvm.debug.args>-agentlib:jdwp=transport=dt_socket,server=y,suspend=${app.server.debug.suspend},address=${app.server.host}:${app.server.debug.port}</app.server.karaf.jvm.debug.args>
</properties>
<profiles>
@ -53,6 +54,7 @@
<exists>src</exists>
</file>
</activation>
<!--
<build>
<plugins>
<plugin>
@ -76,6 +78,7 @@
</plugin>
</plugins>
</build>
-->
</profile>

View file

@ -215,6 +215,7 @@
<app.server.startup.timeout>${app.server.startup.timeout}</app.server.startup.timeout>
<app.server.memory.settings>${app.server.memory.settings}</app.server.memory.settings>
<app.server.jboss.jvm.debug.args>${app.server.jboss.jvm.debug.args}</app.server.jboss.jvm.debug.args>
<app.server.karaf.jvm.debug.args>${app.server.karaf.jvm.debug.args}</app.server.karaf.jvm.debug.args>
<app.server.reverse-proxy.port.offset>${app.server.reverse-proxy.port.offset}</app.server.reverse-proxy.port.offset>