This is an automated email from the ASF dual-hosted git repository. rzo1 pushed a commit to branch livereload-loopback-only in repository https://gitbox.apache.org/repos/asf/tomee.git
commit ecd535c9b9ec255d8a8b80298f87ecc9364a2393 Author: Richard Zowalla <[email protected]> AuthorDate: Wed Oct 7 20:42:04 2026 +0200 bind livereload connector to loopback and check websocket origin The dev-time livereload endpoint was reachable from the network and from any web page. --- docs/developer/tools/maven/embedded.adoc | 2 +- .../apache/openejb/maven/plugins/LiveReload.java | 18 ++++++ .../openejb/maven/plugins/TomEEEmbeddedMojo.java | 3 +- .../org/apache/tomee/livereload/Instances.java | 9 +++ .../tomee/livereload/LiveReloadEndpoint.java | 2 +- .../tomee/livereload/LiveReloadInstaller.java | 17 +++++- .../livereload/LoopbackOriginConfigurator.java | 60 ++++++++++++++++++ .../livereload/LoopbackOriginConfiguratorTest.java | 71 ++++++++++++++++++++++ 8 files changed, 177 insertions(+), 5 deletions(-) diff --git a/docs/developer/tools/maven/embedded.adoc b/docs/developer/tools/maven/embedded.adoc index e15d3bd4dc..380b98cade 100644 --- a/docs/developer/tools/maven/embedded.adoc +++ b/docs/developer/tools/maven/embedded.adoc @@ -48,6 +48,6 @@ TomEE Embedded Maven plugin has a single goal: `tomee-embedded:run`. | workDir | - | tomee embedded work dir | inlinedServerXml | - | server.xml content directly in the pom | inlinedTomEEXml | - | tomee.xml content directly in the pom -| liveReload | - | livereload configuration if activated. This is an object containing these options: {watchedFolder: 'src/main/webapp', path: '/', port: 35729} +| liveReload | - | livereload configuration if activated. This is an object containing these options: {watchedFolder: 'src/main/webapp', path: '/', port: 35729, host: 'localhost', allowAnyOrigin: false}. By default the livereload connector only listens on the loopback interface and rejects websocket handshakes from non loopback page origins, set `host` (for instance to `0.0.0.0`) and `allowAnyOrigin` to `true` to use it from another machine | withLiveReload | false | activate livereload for web resources |=== diff --git a/maven/tomee-embedded-maven-plugin/src/main/java/org/apache/openejb/maven/plugins/LiveReload.java b/maven/tomee-embedded-maven-plugin/src/main/java/org/apache/openejb/maven/plugins/LiveReload.java index db37f5a22b..efcd1b67f3 100644 --- a/maven/tomee-embedded-maven-plugin/src/main/java/org/apache/openejb/maven/plugins/LiveReload.java +++ b/maven/tomee-embedded-maven-plugin/src/main/java/org/apache/openejb/maven/plugins/LiveReload.java @@ -20,6 +20,8 @@ public class LiveReload { private String watchedFolder; private String path = "/"; // then endpoint is bound to /livereload so to match default we need to set it to ROOT private int port = 35729; + private String host = "localhost"; // address the connector binds to + private boolean allowAnyOrigin; // accept websocket handshakes from non loopback pages public String getWatchedFolder() { return watchedFolder; @@ -44,4 +46,20 @@ public class LiveReload { public void setPort(final int port) { this.port = port; } + + public String getHost() { + return host; + } + + public void setHost(final String host) { + this.host = host; + } + + public boolean isAllowAnyOrigin() { + return allowAnyOrigin; + } + + public void setAllowAnyOrigin(final boolean allowAnyOrigin) { + this.allowAnyOrigin = allowAnyOrigin; + } } diff --git a/maven/tomee-embedded-maven-plugin/src/main/java/org/apache/openejb/maven/plugins/TomEEEmbeddedMojo.java b/maven/tomee-embedded-maven-plugin/src/main/java/org/apache/openejb/maven/plugins/TomEEEmbeddedMojo.java index cf28f25d2a..cf7866f00f 100644 --- a/maven/tomee-embedded-maven-plugin/src/main/java/org/apache/openejb/maven/plugins/TomEEEmbeddedMojo.java +++ b/maven/tomee-embedded-maven-plugin/src/main/java/org/apache/openejb/maven/plugins/TomEEEmbeddedMojo.java @@ -586,7 +586,8 @@ public class TomEEEmbeddedMojo extends AbstractMojo { if (liveReload != null) { LiveReloadInstaller.install( liveReload.getPath(), liveReload.getPort(), - liveReload.getWatchedFolder() == null ? docBase.getAbsolutePath() : liveReload.getWatchedFolder()); + liveReload.getWatchedFolder() == null ? docBase.getAbsolutePath() : liveReload.getWatchedFolder(), + liveReload.getHost(), liveReload.isAllowAnyOrigin()); } } diff --git a/utils/livereload-tomee/src/main/java/org/apache/tomee/livereload/Instances.java b/utils/livereload-tomee/src/main/java/org/apache/tomee/livereload/Instances.java index be5944a98c..ae25d5ef1d 100644 --- a/utils/livereload-tomee/src/main/java/org/apache/tomee/livereload/Instances.java +++ b/utils/livereload-tomee/src/main/java/org/apache/tomee/livereload/Instances.java @@ -35,6 +35,7 @@ public class Instances { private final LogCategory logCategory = LogCategory.OPENEJB.createChild("livereload"); private final Mapper mapper = new MapperBuilder().build(); private final FileWatcher watcher = new FileWatcher(logCategory, mapper); + private volatile boolean allowAnyOrigin; public FileWatcher getWatcher() { return watcher; @@ -47,4 +48,12 @@ public class Instances { public LogCategory getLogCategory() { return logCategory; } + + public boolean isAllowAnyOrigin() { + return allowAnyOrigin; + } + + public void setAllowAnyOrigin(final boolean allowAnyOrigin) { + this.allowAnyOrigin = allowAnyOrigin; + } } diff --git a/utils/livereload-tomee/src/main/java/org/apache/tomee/livereload/LiveReloadEndpoint.java b/utils/livereload-tomee/src/main/java/org/apache/tomee/livereload/LiveReloadEndpoint.java index 54e7bbef87..720149e78a 100644 --- a/utils/livereload-tomee/src/main/java/org/apache/tomee/livereload/LiveReloadEndpoint.java +++ b/utils/livereload-tomee/src/main/java/org/apache/tomee/livereload/LiveReloadEndpoint.java @@ -29,7 +29,7 @@ import java.io.IOException; import static java.util.Arrays.asList; -@ServerEndpoint("/livereload") +@ServerEndpoint(value = "/livereload", configurator = LoopbackOriginConfigurator.class) public class LiveReloadEndpoint { private static final Command HELLO = new Command(); static { diff --git a/utils/livereload-tomee/src/main/java/org/apache/tomee/livereload/LiveReloadInstaller.java b/utils/livereload-tomee/src/main/java/org/apache/tomee/livereload/LiveReloadInstaller.java index be60431ef1..1b148cc76c 100644 --- a/utils/livereload-tomee/src/main/java/org/apache/tomee/livereload/LiveReloadInstaller.java +++ b/utils/livereload-tomee/src/main/java/org/apache/tomee/livereload/LiveReloadInstaller.java @@ -39,11 +39,22 @@ import java.util.Collections; public class LiveReloadInstaller { + public static final String DEFAULT_HOST = "localhost"; + private LiveReloadInstaller() { // no-op } - public static void install(String path, final int port, final String folder) { + public static void install(final String path, final int port, final String folder) { + install(path, port, folder, null, false); + } + + /** + * @param address the address the livereload connector binds to, loopback ("localhost") when null or empty + * @param allowAnyOrigin if false websocket handshakes coming from a non loopback page origin are rejected + */ + public static void install(final String path, final int port, final String folder, + final String address, final boolean allowAnyOrigin) { final Server server = TomcatHelper.getServer(); if (server == null) { throw new IllegalStateException("tomcat not yet starting"); @@ -56,10 +67,12 @@ public class LiveReloadInstaller { throw new IllegalStateException("host not started, call LiveReloadInstaller.install() later."); } - // add connector + // add connector, dev only so loopback by default final Connector connector = new Connector(); connector.setPort(port); + connector.setProperty("address", address == null || address.isEmpty() ? DEFAULT_HOST : address); connector.setProperty("connectionTimeout", "30000"); + Instances.get().setAllowAnyOrigin(allowAnyOrigin); service.addConnector(connector); // and the endpoint and start the watcher diff --git a/utils/livereload-tomee/src/main/java/org/apache/tomee/livereload/LoopbackOriginConfigurator.java b/utils/livereload-tomee/src/main/java/org/apache/tomee/livereload/LoopbackOriginConfigurator.java new file mode 100644 index 0000000000..ab9ee362fb --- /dev/null +++ b/utils/livereload-tomee/src/main/java/org/apache/tomee/livereload/LoopbackOriginConfigurator.java @@ -0,0 +1,60 @@ +/* + * 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.tomee.livereload; + +import jakarta.websocket.server.ServerEndpointConfig; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Locale; + +/** + * Only accepts websocket handshakes from loopback pages, browser extensions + * and non browser clients (no Origin header) unless any origin was explicitly allowed. + */ +public class LoopbackOriginConfigurator extends ServerEndpointConfig.Configurator { + @Override + public boolean checkOrigin(final String originHeaderValue) { + return Instances.get().isAllowAnyOrigin() || isLocalOrigin(originHeaderValue); + } + + static boolean isLocalOrigin(final String origin) { + if (origin == null || origin.isEmpty()) { + return true; + } + + final URI uri; + try { + uri = new URI(origin); + } catch (final URISyntaxException e) { + return false; + } + + final String scheme = uri.getScheme() == null ? "" : uri.getScheme().toLowerCase(Locale.ROOT); + switch (scheme) { + case "chrome-extension": + case "moz-extension": + case "safari-web-extension": + return true; + case "http": + case "https": + final String host = uri.getHost() == null ? "" : uri.getHost().toLowerCase(Locale.ROOT); + return "localhost".equals(host) || "127.0.0.1".equals(host) || "[::1]".equals(host); + default: + return false; + } + } +} diff --git a/utils/livereload-tomee/src/test/java/org/apache/tomee/livereload/LoopbackOriginConfiguratorTest.java b/utils/livereload-tomee/src/test/java/org/apache/tomee/livereload/LoopbackOriginConfiguratorTest.java new file mode 100644 index 0000000000..be33bed580 --- /dev/null +++ b/utils/livereload-tomee/src/test/java/org/apache/tomee/livereload/LoopbackOriginConfiguratorTest.java @@ -0,0 +1,71 @@ +/* + * 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.tomee.livereload; + +import org.junit.After; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class LoopbackOriginConfiguratorTest { + private final LoopbackOriginConfigurator configurator = new LoopbackOriginConfigurator(); + + @After + public void reset() { + Instances.get().setAllowAnyOrigin(false); + } + + @Test + public void acceptsMissingOrigin() { + assertTrue(configurator.checkOrigin(null)); + assertTrue(configurator.checkOrigin("")); + } + + @Test + public void acceptsLoopbackOrigins() { + assertTrue(configurator.checkOrigin("http://localhost:8080")); + assertTrue(configurator.checkOrigin("https://LOCALHOST")); + assertTrue(configurator.checkOrigin("http://127.0.0.1:8080")); + assertTrue(configurator.checkOrigin("http://[::1]:8080")); + } + + @Test + public void acceptsBrowserExtensions() { + assertTrue(configurator.checkOrigin("chrome-extension://abcdefghijklmnop")); + assertTrue(configurator.checkOrigin("moz-extension://0f8cf3a6-6c8e-4b2a-8e5d-9f4a4b2f1c3d")); + } + + @Test + public void rejectsOtherOrigins() { + assertFalse(configurator.checkOrigin("http://example.com")); + assertFalse(configurator.checkOrigin("https://example.com:35729")); + assertFalse(configurator.checkOrigin("http://localhost.example.com")); + assertFalse(configurator.checkOrigin("http://192.168.1.10:8080")); + assertFalse(configurator.checkOrigin("null")); + assertFalse(configurator.checkOrigin("file://localhost")); + assertFalse(configurator.checkOrigin("not a uri ::")); + assertFalse(configurator.checkOrigin("http://")); + } + + @Test + public void acceptsAnyOriginWhenAllowed() { + Instances.get().setAllowAnyOrigin(true); + assertTrue(configurator.checkOrigin("http://example.com")); + assertTrue(configurator.checkOrigin("http://192.168.1.10:8080")); + } +}
