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";));
+    }
+}

Reply via email to