This is an automated email from the ASF dual-hosted git repository.

ddanielr pushed a commit to branch 2.1
in repository https://gitbox.apache.org/repos/asf/accumulo.git


The following commit(s) were added to refs/heads/2.1 by this push:
     new 8779467e73 Allows the monitor to be hosted behind a proxy (#5729)
8779467e73 is described below

commit 8779467e73cc3b09b512499ee669a31353ea7454
Author: Daniel Roberts <[email protected]>
AuthorDate: Thu Jul 17 11:28:03 2025 -0400

    Allows the monitor to be hosted behind a proxy (#5729)
    
    Changes the templates to use a base href path vs hardcoded absolute
    paths and changes links in js files to use the base href value.
    
    Allows the user to set a system property to define the root context of
    the monitor.
    
    Update the js files and functions created by FreeMarker to use the
    contextPath when making requests.
    
    Adds test for EmbeddedWebServer to check if the contextPath is correct.
    Also adds preconditions to Monitor.java to check the format of the root
    context value.
---
 assemble/pom.xml                                   |  5 ++
 .../org/apache/accumulo/core/conf/Property.java    |  5 ++
 pom.xml                                            |  5 ++
 server/monitor/pom.xml                             |  4 +
 .../apache/accumulo/monitor/EmbeddedWebServer.java | 21 ++++-
 .../java/org/apache/accumulo/monitor/Monitor.java  | 11 ++-
 .../org/apache/accumulo/monitor/view/WebViews.java |  8 +-
 .../accumulo/monitor/resources/js/bulkImport.js    |  4 +-
 .../accumulo/monitor/resources/js/compactions.js   |  4 +-
 .../org/apache/accumulo/monitor/resources/js/ec.js |  8 +-
 .../accumulo/monitor/resources/js/functions.js     | 60 +++++++-------
 .../org/apache/accumulo/monitor/resources/js/gc.js |  2 +-
 .../apache/accumulo/monitor/resources/js/global.js |  2 +
 .../accumulo/monitor/resources/js/manager.js       |  6 +-
 .../accumulo/monitor/resources/js/problems.js      |  8 +-
 .../accumulo/monitor/resources/js/replication.js   |  2 +-
 .../apache/accumulo/monitor/resources/js/scans.js  |  4 +-
 .../apache/accumulo/monitor/resources/js/server.js |  4 +-
 .../apache/accumulo/monitor/resources/js/table.js  |  4 +-
 .../accumulo/monitor/resources/js/tservers.js      |  8 +-
 .../apache/accumulo/monitor/templates/default.ftl  | 49 ++++++------
 .../org/apache/accumulo/monitor/templates/log.ftl  |  2 +-
 .../apache/accumulo/monitor/templates/modals.ftl   |  2 +-
 .../apache/accumulo/monitor/templates/navbar.ftl   | 30 +++----
 .../apache/accumulo/monitor/templates/overview.ftl |  8 +-
 .../apache/accumulo/monitor/templates/tables.ftl   |  4 +-
 .../accumulo/monitor/EmbeddedWebServerTest.java    | 93 ++++++++++++++++++++++
 27 files changed, 256 insertions(+), 107 deletions(-)

diff --git a/assemble/pom.xml b/assemble/pom.xml
index 2d933e582f..1236bbb9c1 100644
--- a/assemble/pom.xml
+++ b/assemble/pom.xml
@@ -126,6 +126,11 @@
       <artifactId>commons-logging</artifactId>
       <optional>true</optional>
     </dependency>
+    <dependency>
+      <groupId>commons-validator</groupId>
+      <artifactId>commons-validator</artifactId>
+      <optional>true</optional>
+    </dependency>
     <dependency>
       <groupId>io.micrometer</groupId>
       <artifactId>micrometer-commons</artifactId>
diff --git a/core/src/main/java/org/apache/accumulo/core/conf/Property.java 
b/core/src/main/java/org/apache/accumulo/core/conf/Property.java
index d9d94cb367..625cb0ed1f 100644
--- a/core/src/main/java/org/apache/accumulo/core/conf/Property.java
+++ b/core/src/main/java/org/apache/accumulo/core/conf/Property.java
@@ -1037,6 +1037,11 @@ public enum Property {
           + " The resources that are used by default can be seen in"
           + " 
`accumulo/server/monitor/src/main/resources/templates/default.ftl`.",
       "2.0.0"),
+  MONITOR_ROOT_CONTEXT("monitor.root.context", "/", PropertyType.STRING,
+      "The root context path of the monitor application. If this value is set, 
all paths for the"
+          + " monitor application will be hosted using this context. As an 
example, setting this to `/accumulo/`"
+          + " would cause all `/rest/` endpoints to be hosted at 
`/accumulo/rest/*`.",
+      "2.1.4"),
   @Deprecated(since = "2.1.0")
   TRACE_PREFIX("trace.", null, PropertyType.PREFIX,
       "Properties in this category affect the behavior of distributed 
tracing.", "1.3.5"),
diff --git a/pom.xml b/pom.xml
index c8aa687bbb..fdcab6190e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -322,6 +322,11 @@
         <artifactId>commons-logging</artifactId>
         <version>1.3.5</version>
       </dependency>
+      <dependency>
+        <groupId>commons-validator</groupId>
+        <artifactId>commons-validator</artifactId>
+        <version>1.10.0</version>
+      </dependency>
       <dependency>
         <!-- legacy junit version specified here for dependency convergence -->
         <groupId>junit</groupId>
diff --git a/server/monitor/pom.xml b/server/monitor/pom.xml
index cb2e899597..23ecd0d691 100644
--- a/server/monitor/pom.xml
+++ b/server/monitor/pom.xml
@@ -52,6 +52,10 @@
       <groupId>com.google.guava</groupId>
       <artifactId>guava</artifactId>
     </dependency>
+    <dependency>
+      <groupId>commons-validator</groupId>
+      <artifactId>commons-validator</artifactId>
+    </dependency>
     <dependency>
       <groupId>jakarta.inject</groupId>
       <artifactId>jakarta.inject-api</artifactId>
diff --git 
a/server/monitor/src/main/java/org/apache/accumulo/monitor/EmbeddedWebServer.java
 
b/server/monitor/src/main/java/org/apache/accumulo/monitor/EmbeddedWebServer.java
index 696cbe6a96..8249d231c0 100644
--- 
a/server/monitor/src/main/java/org/apache/accumulo/monitor/EmbeddedWebServer.java
+++ 
b/server/monitor/src/main/java/org/apache/accumulo/monitor/EmbeddedWebServer.java
@@ -22,6 +22,7 @@ import java.util.EnumSet;
 
 import org.apache.accumulo.core.conf.AccumuloConfiguration;
 import org.apache.accumulo.core.conf.Property;
+import org.apache.commons.validator.routines.UrlValidator;
 import org.eclipse.jetty.server.AbstractConnectionFactory;
 import org.eclipse.jetty.server.HttpConnectionFactory;
 import org.eclipse.jetty.server.Server;
@@ -33,6 +34,9 @@ import org.eclipse.jetty.util.ssl.SslContextFactory;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+
 public class EmbeddedWebServer {
   private static final Logger LOG = 
LoggerFactory.getLogger(EmbeddedWebServer.class);
 
@@ -57,7 +61,17 @@ public class EmbeddedWebServer {
     handler =
         new ServletContextHandler(ServletContextHandler.SESSIONS | 
ServletContextHandler.SECURITY);
     handler.getSessionHandler().getSessionCookieConfig().setHttpOnly(true);
-    handler.setContextPath("/");
+    String rootContext = 
monitor.getConfiguration().get(Property.MONITOR_ROOT_CONTEXT);
+    // Validate URL
+    String[] scheme = {"https"};
+    UrlValidator validator = new UrlValidator(scheme);
+    Preconditions.checkArgument(validator.isValid("https://example.com"; + 
rootContext),
+        "Root context: \"%s\" is not a valid URL", rootContext);
+    // Remove the trailing slash since jetty will warn otherwise.
+    if (rootContext.length() > 1 && rootContext.endsWith("/")) {
+      rootContext = rootContext.substring(0, rootContext.length() - 1);
+    }
+    handler.setContextPath(rootContext);
   }
 
   private static AbstractConnectionFactory[] 
getConnectionFactories(AccumuloConfiguration conf,
@@ -108,6 +122,11 @@ public class EmbeddedWebServer {
     handler.addServlet(restServlet, where);
   }
 
+  @VisibleForTesting
+  String getContextPath() {
+    return handler.getContextPath();
+  }
+
   public String getHostName() {
     return connector.getHost();
   }
diff --git 
a/server/monitor/src/main/java/org/apache/accumulo/monitor/Monitor.java 
b/server/monitor/src/main/java/org/apache/accumulo/monitor/Monitor.java
index f798ad3c30..f9672ed196 100644
--- a/server/monitor/src/main/java/org/apache/accumulo/monitor/Monitor.java
+++ b/server/monitor/src/main/java/org/apache/accumulo/monitor/Monitor.java
@@ -105,6 +105,7 @@ import org.glassfish.jersey.servlet.ServletContainer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.base.Preconditions;
 import com.google.common.base.Suppliers;
 
 /**
@@ -453,6 +454,10 @@ public class Monitor extends AbstractServer implements 
HighlyAvailableService {
   public void run() {
     ServerContext context = getContext();
     int[] ports = getConfiguration().getPort(Property.MONITOR_PORT);
+    String rootContext = getConfiguration().get(Property.MONITOR_ROOT_CONTEXT);
+    // Needs leading slash in order to property create rest endpoint requests
+    Preconditions.checkArgument(rootContext.startsWith("/"),
+        "Root context: \"%s\" does not have a leading '/'", rootContext);
     for (int port : ports) {
       try {
         log.debug("Trying monitor on port {}", port);
@@ -508,9 +513,13 @@ public class Monitor extends AbstractServer implements 
HighlyAvailableService {
     metricsInfo.init(MetricsInfo.serviceTags(getContext().getInstanceName(), 
getApplicationName(),
         monitorHostAndPort, ""));
 
+    // Needed to support the existing zk monitor address format
+    if (!rootContext.endsWith("/")) {
+      rootContext = rootContext + "/";
+    }
     try {
       URL url = new URL(server.isSecure() ? "https" : "http", 
monitorHostAndPort.getHost(),
-          server.getPort(), "/");
+          server.getPort(), rootContext);
       final String path = context.getZooKeeperRoot() + 
Constants.ZMONITOR_HTTP_ADDR;
       final ZooReaderWriter zoo = context.getZooReaderWriter();
       // Delete before we try to re-create in case the previous session hasn't 
yet expired
diff --git 
a/server/monitor/src/main/java/org/apache/accumulo/monitor/view/WebViews.java 
b/server/monitor/src/main/java/org/apache/accumulo/monitor/view/WebViews.java
index cb69bf9e73..92b9701709 100644
--- 
a/server/monitor/src/main/java/org/apache/accumulo/monitor/view/WebViews.java
+++ 
b/server/monitor/src/main/java/org/apache/accumulo/monitor/view/WebViews.java
@@ -93,12 +93,18 @@ public class WebViews {
   }
 
   private Map<String,Object> getModel() {
-
+    AccumuloConfiguration conf = monitor.getContext().getConfiguration();
+    String rootContext = conf.get(Property.MONITOR_ROOT_CONTEXT);
+    // Add trailing slash if it doesn't exist
+    if (!rootContext.endsWith("/")) {
+      rootContext = rootContext + "/";
+    }
     Map<String,Object> model = new HashMap<>();
     model.put("version", Constants.VERSION);
     model.put("instance_name", monitor.getContext().getInstanceName());
     model.put("instance_id", monitor.getContext().getInstanceID());
     model.put("zk_hosts", monitor.getContext().getZooKeepers());
+    model.put("rootContext", rootContext);
     addExternalResources(model);
     return model;
   }
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/bulkImport.js
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/bulkImport.js
index dbc9350c35..a89bc95ad4 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/bulkImport.js
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/bulkImport.js
@@ -40,7 +40,7 @@ function refresh() {
  */
 $(document).ready(function () {
 
-  const url = '/rest/bulkImports';
+  const url = contextPath + 'rest/bulkImports';
   console.debug('REST url used to fetch data for the DataTables in 
bulkImport.js: ' + url);
 
   // Generates the manager bulk import status table
@@ -85,7 +85,7 @@ $(document).ready(function () {
         "type": "html",
         "render": function (data, type) {
           if (type === 'display') {
-            data = `<a href="/tservers?s=${data}">${data}</a>`;
+            data = `<a href="tservers?s=${data}">${data}</a>`;
           }
           return data;
         }
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/compactions.js
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/compactions.js
index 1275518c20..e0737de9e5 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/compactions.js
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/compactions.js
@@ -26,7 +26,7 @@ $(document).ready(function () {
   // Create a table for compactions list
   compactionsList = $('#compactionsList').DataTable({
     "ajax": {
-      "url": '/rest/compactions',
+      "url":  contextPath + 'rest/compactions',
       "dataSrc": "compactions"
     },
     "stateSave": true,
@@ -51,7 +51,7 @@ $(document).ready(function () {
         "type": "html",
         "render": function (data, type, row, meta) {
           if (type === 'display') {
-            data = '<a href="/tservers?s=' + row.server + '">' + row.server + 
'</a>';
+            data = '<a href="tservers?s=' + row.server + '">' + row.server + 
'</a>';
           }
           return data;
         }
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/ec.js
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/ec.js
index 545c2559df..9ec7c98339 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/ec.js
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/ec.js
@@ -37,7 +37,7 @@ $(document).ready(function () {
 
   compactorsTable = $('#compactorsTable').DataTable({
     "ajax": {
-      "url": '/rest/ec/compactors',
+      "url": contextPath + 'rest/ec/compactors',
       "dataSrc": "compactors"
     },
     "stateSave": true,
@@ -72,7 +72,7 @@ $(document).ready(function () {
   // Create a table for running compactors
   runningTable = $('#runningTable').DataTable({
     "ajax": {
-      "url": '/rest/ec/running',
+      "url": contextPath + 'rest/ec/running',
       "dataSrc": "running"
     },
     "stateSave": true,
@@ -145,7 +145,7 @@ $(document).ready(function () {
   // Create a table for compaction coordinator
   coordinatorTable = $('#coordinatorTable').DataTable({
     "ajax": {
-      "url": '/rest/ec',
+      "url": contextPath + 'rest/ec',
       "dataSrc": function (data) {
         // the data needs to be in an array to work with DataTables
         var arr = [];
@@ -275,7 +275,7 @@ async function refreshCoordinatorStatus() {
 }
 
 function getRunningDetails(ecid, idSuffix) {
-  var ajaxUrl = '/rest/ec/details?ecid=' + ecid;
+  var ajaxUrl = contextPath + 'rest/ec/details?ecid=' + ecid;
   console.log("Ajax call to " + ajaxUrl);
   $.getJSON(ajaxUrl, function (data) {
     populateDetails(data, idSuffix);
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/functions.js
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/functions.js
index 36a46f69e8..30705be653 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/functions.js
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/functions.js
@@ -340,21 +340,21 @@ function doLoggedPostCall(call, callback, shouldSanitize) 
{
  * stores it on a sessionStorage variable
  */
 function getManager() {
-  return getJSONForTable('/rest/manager', 'manager');
+  return getJSONForTable(contextPath + 'rest/manager', 'manager');
 }
 
 /**
  * REST GET call for the namespaces, stores it on a global variable
  */
 function getNamespaces() {
-  return getJSONForTable('/rest/tables/namespaces', 'NAMESPACES');
+  return getJSONForTable(contextPath + 'rest/tables/namespaces', 'NAMESPACES');
 }
 
 /**
  * REST GET call for the tables, stores it on a sessionStorage variable
  */
 function getTables() {
-  return getJSONForTable('/rest/tables', 'tables');
+  return getJSONForTable(contextPath + 'rest/tables', 'tables');
 }
 
 /**
@@ -378,7 +378,7 @@ function getNamespaceTables(namespaces) {
   // Convert the list to a string for the REST call
   namespaceList = namespaces.toString();
 
-  return getJSONForTable('/rest/tables/namespaces/' + namespaceList, 'tables');
+  return getJSONForTable(contextPath + 'rest/tables/namespaces/' + 
namespaceList, 'tables');
 }
 
 /**
@@ -387,14 +387,14 @@ function getNamespaceTables(namespaces) {
  * @param {string} server Dead Server ID
  */
 function clearDeadServers(server) {
-  doLoggedPostCall('/rest/tservers?server=' + server, null, false);
+  doLoggedPostCall(contextPath + 'rest/tservers?server=' + server, null, 
false);
 }
 
 /**
  * REST GET call for the tservers, stores it on a sessionStorage variable
  */
 function getTServers() {
-  return getJSONForTable('/rest/tservers', 'tservers');
+  return getJSONForTable(contextPath + 'rest/tservers', 'tservers');
 }
 
 /**
@@ -403,35 +403,35 @@ function getTServers() {
  * @param {string} server Server ID
  */
 function getTServer(server) {
-  return getJSONForTable('/rest/tservers/' + server, 'server');
+  return getJSONForTable(contextPath + 'rest/tservers/' + server, 'server');
 }
 
 /**
  * REST GET call for the scans, stores it on a sessionStorage variable
  */
 function getScans() {
-  return getJSONForTable('/rest/scans', 'scans');
+  return getJSONForTable(contextPath + 'rest/scans', 'scans');
 }
 
 /**
  * REST GET call for the bulk imports, stores it on a sessionStorage variable
  */
 function getBulkImports() {
-  return getJSONForTable('/rest/bulkImports', 'bulkImports');
+  return getJSONForTable(contextPath + 'rest/bulkImports', 'bulkImports');
 }
 
 /**
  * REST GET call for the server stats, stores it on a sessionStorage variable
  */
 function getServerStats() {
-  return getJSONForTable('/rest/tservers/serverStats', 'serverStats');
+  return getJSONForTable(contextPath + 'rest/tservers/serverStats', 
'serverStats');
 }
 
 /**
  * REST GET call for the recovery list, stores it on a sessionStorage variable
  */
 function getRecoveryList() {
-  return getJSONForTable('/rest/tservers/recovery', 'recoveryList');
+  return getJSONForTable(contextPath + 'rest/tservers/recovery', 
'recoveryList');
 }
 
 /**
@@ -441,21 +441,21 @@ function getRecoveryList() {
  * @param {string} table Table ID
  */
 function getTableServers(tableID) {
-  return getJSONForTable('/rest/tables/' + tableID, 'tableServers');
+  return getJSONForTable(contextPath + 'rest/tables/' + tableID, 
'tableServers');
 }
 
 /**
  * REST GET call for the logs, stores it on a sessionStorage variable
  */
 function getLogs() {
-  return getJSONForTable('/rest/logs', 'logs');
+  return getJSONForTable(contextPath + 'rest/logs', 'logs');
 }
 
 /**
  * REST POST call to clear logs
  */
 function clearLogs() {
-  doLoggedPostCall('/rest/logs/clear', refresh, false);
+  doLoggedPostCall(contextPath + 'rest/logs/clear', refresh, false);
 }
 
 /**
@@ -464,7 +464,7 @@ function clearLogs() {
  * @param {string} tableID Table ID
  */
 function clearTableProblems(tableID) {
-  doLoggedPostCall('/rest/problems/summary?s=' + tableID, refresh, true);
+  doLoggedPostCall(contextPath + 'rest/problems/summary?s=' + tableID, 
refresh, true);
 }
 
 /**
@@ -475,7 +475,7 @@ function clearTableProblems(tableID) {
  * @param {string} type Type of problem
  */
 function clearDetailsProblems(table, resource, type) {
-  doLoggedPostCall('/rest/problems/details?table=' + table + '&resource=' +
+  doLoggedPostCall(contextPath + 'rest/problems/details?table=' + table + 
'&resource=' +
     resource + '&ptype=' + type, refresh, true);
 }
 
@@ -484,7 +484,7 @@ function clearDetailsProblems(table, resource, type) {
  * stores it on a sessionStorage variable
  */
 function getProblemSummary() {
-  return getJSONForTable('/rest/problems/summary', 'problemSummary');
+  return getJSONForTable(contextPath + 'rest/problems/summary', 
'problemSummary');
 }
 
 /**
@@ -492,7 +492,7 @@ function getProblemSummary() {
  * stores it on a sessionStorage variable
  */
 function getProblemDetails() {
-  return getJSONForTable('/rest/problems/details', 'problemDetails');
+  return getJSONForTable(contextPath + 'rest/problems/details', 
'problemDetails');
 }
 
 /**
@@ -500,7 +500,7 @@ function getProblemDetails() {
  * stores it on a sessionStorage variable
  */
 function getReplication() {
-  return getJSONForTable('/rest/replication', 'replication');
+  return getJSONForTable(contextPath + 'rest/replication', 'replication');
 }
 
 //// Overview Plots Rest Calls
@@ -510,7 +510,7 @@ function getReplication() {
  * stores it on a sessionStorage variable
  */
 function getIngestRate() {
-  return getJSONForTable('/rest/statistics/time/ingestRate', 'ingestRate');
+  return getJSONForTable(contextPath + 'rest/statistics/time/ingestRate', 
'ingestRate');
 }
 
 /**
@@ -518,7 +518,7 @@ function getIngestRate() {
  * stores it on a sessionStorage variable
  */
 function getScanEntries() {
-  return getJSONForTable('/rest/statistics/time/scanEntries', 'scanEntries');
+  return getJSONForTable(contextPath + 'rest/statistics/time/scanEntries', 
'scanEntries');
 }
 
 /**
@@ -526,28 +526,28 @@ function getScanEntries() {
  * stores it on a sessionStorage variable
  */
 function getIngestByteRate() {
-  return getJSONForTable('/rest/statistics/time/ingestByteRate', 'ingestMB');
+  return getJSONForTable(contextPath + 'rest/statistics/time/ingestByteRate', 
'ingestMB');
 }
 
 /**
  * REST GET call for the query byte rate, stores it on a sessionStorage 
variable
  */
 function getQueryByteRate() {
-  return getJSONForTable('/rest/statistics/time/queryByteRate', 'queryMB');
+  return getJSONForTable(contextPath + 'rest/statistics/time/queryByteRate', 
'queryMB');
 }
 
 /**
  * REST GET call for the load average, stores it on a sessionStorage variable
  */
 function getLoadAverage() {
-  return getJSONForTable('/rest/statistics/time/load', 'loadAvg');
+  return getJSONForTable(contextPath + 'rest/statistics/time/load', 'loadAvg');
 }
 
 /**
  * REST GET call for the lookups, stores it on a sessionStorage variable
  */
 function getLookups() {
-  return getJSONForTable('/rest/statistics/time/lookups', 'lookups');
+  return getJSONForTable(contextPath + 'rest/statistics/time/lookups', 
'lookups');
 }
 
 /**
@@ -555,7 +555,7 @@ function getLookups() {
  * stores it on a sessionStorage variable
  */
 function getMinorCompactions() {
-  return getJSONForTable('/rest/statistics/time/minorCompactions', 
'minorCompactions');
+  return getJSONForTable(contextPath + 
'rest/statistics/time/minorCompactions', 'minorCompactions');
 }
 
 /**
@@ -563,7 +563,7 @@ function getMinorCompactions() {
  * stores it on a sessionStorage variable
  */
 function getMajorCompactions() {
-  return getJSONForTable('/rest/statistics/time/majorCompactions', 
'majorCompactions');
+  return getJSONForTable(contextPath + 
'rest/statistics/time/majorCompactions', 'majorCompactions');
 }
 
 /**
@@ -571,7 +571,7 @@ function getMajorCompactions() {
  * stores it on a sessionStorage variable
  */
 function getIndexCacheHitRate() {
-  return getJSONForTable('/rest/statistics/time/indexCacheHitRate', 
'indexCache');
+  return getJSONForTable(contextPath + 
'rest/statistics/time/indexCacheHitRate', 'indexCache');
 }
 
 /**
@@ -579,14 +579,14 @@ function getIndexCacheHitRate() {
  * stores it on a sessionStorage variable
  */
 function getDataCacheHitRate() {
-  return getJSONForTable('/rest/statistics/time/dataCacheHitRate', 
'dataCache');
+  return getJSONForTable(contextPath + 
'rest/statistics/time/dataCacheHitRate', 'dataCache');
 }
 
 /**
  * REST GET call for the server status, stores it on a sessionStorage variable
  */
 function getStatus() {
-  return getJSONForTable('/rest/status', 'status');
+  return getJSONForTable(contextPath + 'rest/status', 'status');
 }
 
 /*
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/gc.js
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/gc.js
index 6b1a8d4119..63e9a75307 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/gc.js
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/gc.js
@@ -26,7 +26,7 @@ $(document).ready(function () {
   // Create a table for compactions list
   gcTable = $('#gcActivity').DataTable({
     "ajax": {
-      "url": '/rest/gc',
+      "url": contextPath + 'rest/gc',
       "dataSrc": "stats"
     },
     "stateSave": true,
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/global.js
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/global.js
index d4562b7e03..7b6c33526b 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/global.js
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/global.js
@@ -28,5 +28,7 @@ var NAMESPACES = '';
  */
 var TIMER;
 
+const contextPath = $("base").attr("href");
+
 const EMPTY_CELL = "<td>-</td>";
 const EMPTY_ROW_THREE_CELLS = "<tr>" + EMPTY_CELL + EMPTY_CELL + EMPTY_CELL + 
"</tr>";
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/manager.js
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/manager.js
index 716140c4f1..cdc5942197 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/manager.js
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/manager.js
@@ -95,7 +95,7 @@ $(document).ready(function () {
   // Generates the manager table
   managerStatusTable = $('#managerStatus').DataTable({
     "ajax": {
-      "url": '/rest/manager',
+      "url": contextPath + 'rest/manager',
       "dataSrc": function (json) {
         // the data needs to be in an array to work with DataTables
         var arr = [json];
@@ -151,7 +151,7 @@ $(document).ready(function () {
             if (data !== 'Waiting') {
               data = dateFormat(parseInt(data, 10));
             }
-            data = '<a href="/gc">' + data + '</a>';
+            data = '<a href="gc">' + data + '</a>';
           }
           return data;
         }
@@ -186,7 +186,7 @@ $(document).ready(function () {
   // Generates the recovery table
   recoveryListTable = $('#recoveryList').DataTable({
     "ajax": {
-      "url": '/rest/tservers/recovery',
+      "url": contextPath + 'rest/tservers/recovery',
       "dataSrc": function (data) {
         data = data.recoveryList;
         if (data.length === 0) {
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/problems.js
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/problems.js
index 051a40d06e..9510d073be 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/problems.js
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/problems.js
@@ -25,7 +25,7 @@ $(document).ready(function () {
   // Create a table for summary. See datatables doc for more info on the dom 
property
   problemSummaryTable = $('#problemSummary').DataTable({
     "ajax": {
-      "url": '/rest/problems/summary',
+      "url": contextPath + 'rest/problems/summary',
       "dataSrc": "problemSummary"
     },
     "stateSave": true,
@@ -41,7 +41,7 @@ $(document).ready(function () {
         "data": "tableName",
         "type": "html",
         "render": function (data, type, row, meta) {
-          if (type === 'display') data = '<a href="/tables/' + row.tableID + 
'">' + row.tableName + '</a>';
+          if (type === 'display') data = '<a href="tables/' + row.tableID + 
'">' + row.tableName + '</a>';
           return data;
         }
       },
@@ -68,7 +68,7 @@ $(document).ready(function () {
   // Create a table for details
   problemDetailTable = $('#problemDetails').DataTable({
     "ajax": {
-      "url": '/rest/problems/details',
+      "url": contextPath + 'rest/problems/details',
       "dataSrc": "problemDetails"
     },
     "stateSave": true,
@@ -84,7 +84,7 @@ $(document).ready(function () {
         "data": "tableName",
         "type": "html",
         "render": function (data, type, row, meta) {
-          if (type === 'display') data = '<a href="/tables/' + row.tableID + 
'">' + row.tableName + '</a>';
+          if (type === 'display') data = '<a href="tables/' + row.tableID + 
'">' + row.tableName + '</a>';
           return data;
         }
       },
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/replication.js
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/replication.js
index bdfd2fe9b2..175975646b 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/replication.js
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/replication.js
@@ -41,7 +41,7 @@ $(document).ready(function () {
 
   replicationStatsTable = $('#replicationStats').DataTable({
     "ajax": {
-      "url": "/rest/replication",
+      "url": contextPath + "rest/replication",
       "dataSrc": ""
     },
     "stateSave": true,
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/scans.js
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/scans.js
index d9b1db7934..d2ba29881c 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/scans.js
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/scans.js
@@ -27,7 +27,7 @@ $(document).ready(function () {
   // Create a table for scans list
   scansList = $('#scansList').DataTable({
     "ajax": {
-      "url": '/rest/scans',
+      "url": contextPath + 'rest/scans',
       "dataSrc": "scans"
     },
     "stateSave": true,
@@ -52,7 +52,7 @@ $(document).ready(function () {
         "type": "html",
         "render": function (data, type, row, meta) {
           if (type === 'display') {
-            data = '<a href="/tservers?s=' + row.server + '">' + row.server + 
'</a>';
+            data = '<a href="tservers?s=' + row.server + '">' + row.server + 
'</a>';
           }
           return data;
         }
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/server.js
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/server.js
index 5119bd50f7..91560cc02c 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/server.js
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/server.js
@@ -44,7 +44,7 @@ function refresh() {
  */
 function initServerTables(serv) {
 
-  const url = '/rest/tservers/' + serv;
+  const url = contextPath + 'rest/tservers/' + serv;
   console.debug('REST url used to fetch data for server.js DataTables: ' + 
url);
 
   // Create a table for details on the current server
@@ -253,7 +253,7 @@ function initServerTables(serv) {
         "type": "html",
         "render": function (data, type, row) {
           if (type === 'display') {
-            data = `<a href="/tables/${row.tableID}">${data}</a>`;
+            data = `<a href="tables/${row.tableID}">${data}</a>`;
           }
           return data;
         }
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/table.js
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/table.js
index 569138a53c..9a37094149 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/table.js
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/table.js
@@ -45,7 +45,7 @@ function getQueuedAndRunning(data) {
  */
 function initTableServerTable(tableID) {
 
-  const url = '/rest/tables/' + tableID;
+  const url = contextPath + 'rest/tables/' + tableID;
   console.debug('REST url used to fetch data for table.js DataTable: ' + url);
 
   tableServersTable = $('#participatingTServers').DataTable({
@@ -109,7 +109,7 @@ function initTableServerTable(tableID) {
         "type": "html",
         "render": function (data, type, row) {
           if (type === 'display') {
-            data = `<a href="/tservers?s=${row.id}">${data}</a>`;
+            data = `<a href="tservers?s=${row.id}">${data}</a>`;
           }
           return data;
         }
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/tservers.js
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/tservers.js
index a06fa66090..03b477d41d 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/tservers.js
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/resources/js/tservers.js
@@ -136,7 +136,7 @@ $(document).ready(function () {
   // Create a table for tserver list
   tserversTable = $('#tservers').DataTable({
     "ajax": {
-      "url": '/rest/tservers',
+      "url": contextPath + 'rest/tservers',
       "dataSrc": "servers"
     },
     "stateSave": true,
@@ -195,7 +195,7 @@ $(document).ready(function () {
         "type": "html",
         "render": function (data, type, row) {
           if (type === 'display') {
-            data = '<a href="/tservers?s=' + row.id + '">' + row.hostname + 
'</a>';
+            data = '<a href="tservers?s=' + row.id + '">' + row.hostname + 
'</a>';
           }
           return data;
         }
@@ -280,7 +280,7 @@ $(document).ready(function () {
   // Create a table for deadServers list
   deadTServersTable = $('#deadtservers').DataTable({
     "ajax": {
-      "url": '/rest/tservers',
+      "url": contextPath + 'rest/tservers',
       "dataSrc": "deadServers"
     },
     "stateSave": true,
@@ -318,7 +318,7 @@ $(document).ready(function () {
   // Create a table for badServers list
   badTServersTable = $('#badtservers').DataTable({
     "ajax": {
-      "url": '/rest/tservers',
+      "url": contextPath + 'rest/tservers',
       "dataSrc": "badServers"
     },
     "stateSave": true,
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/default.ftl
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/default.ftl
index 19de384eba..037001569e 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/default.ftl
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/default.ftl
@@ -21,6 +21,7 @@
 <!DOCTYPE html>
 <html>
   <head>
+    <base href="${rootContext}"/>
     <title>${title} - Accumulo ${version}</title>
     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
     <!-- external resources configurable by setting monitor.resources.external 
-->
@@ -30,30 +31,30 @@
         ${val}
       </#list>
     <#else>
-      <script src="/resources/external/jquery/jquery-3.7.1.js"></script>
-      <script 
src="/resources/external/bootstrap/js/bootstrap.bundle.js"></script>
-      <script 
src="/resources/external/datatables/js/jquery.dataTables.js"></script>
-      <script 
src="/resources/external/datatables/js/dataTables.bootstrap5.js"></script>
-      <script src="/resources/external/flot/jquery.canvaswrapper.js"></script>
-      <script src="/resources/external/flot/jquery.colorhelpers.js"></script>
-      <script src="/resources/external/flot/jquery.flot.js"></script>
-      <script src="/resources/external/flot/jquery.flot.saturated.js"></script>
-      <script src="/resources/external/flot/jquery.flot.browser.js"></script>
-      <script 
src="/resources/external/flot/jquery.flot.drawSeries.js"></script>
-      <script 
src="/resources/external/flot/jquery.flot.uiConstants.js"></script>
-      <script src="/resources/external/flot/jquery.flot.legend.js"></script>
-      <script src="/resources/external/flot/jquery.flot.time.js"></script>
-      <script src="/resources/external/flot/jquery.flot.resize.js"></script>
-      <link rel="stylesheet" 
href="/resources/external/bootstrap/css/bootstrap.css" />
-      <link rel="stylesheet" 
href="/resources/external/bootstrap/css/bootstrap-icons.css" />
-      <link rel="stylesheet" 
href="/resources/external/datatables/css/dataTables.bootstrap5.css" />
+      <script src="resources/external/jquery/jquery-3.7.1.js"></script>
+      <script 
src="resources/external/bootstrap/js/bootstrap.bundle.js"></script>
+      <script 
src="resources/external/datatables/js/jquery.dataTables.js"></script>
+      <script 
src="resources/external/datatables/js/dataTables.bootstrap5.js"></script>
+      <script src="resources/external/flot/jquery.canvaswrapper.js"></script>
+      <script src="resources/external/flot/jquery.colorhelpers.js"></script>
+      <script src="resources/external/flot/jquery.flot.js"></script>
+      <script src="resources/external/flot/jquery.flot.saturated.js"></script>
+      <script src="resources/external/flot/jquery.flot.browser.js"></script>
+      <script src="resources/external/flot/jquery.flot.drawSeries.js"></script>
+      <script 
src="resources/external/flot/jquery.flot.uiConstants.js"></script>
+      <script src="resources/external/flot/jquery.flot.legend.js"></script>
+      <script src="resources/external/flot/jquery.flot.time.js"></script>
+      <script src="resources/external/flot/jquery.flot.resize.js"></script>
+      <link rel="stylesheet" 
href="resources/external/bootstrap/css/bootstrap.css" />
+      <link rel="stylesheet" 
href="resources/external/bootstrap/css/bootstrap-icons.css" />
+      <link rel="stylesheet" 
href="resources/external/datatables/css/dataTables.bootstrap5.css" />
     </#if>
 
     <!-- accumulo resources -->
-    <link rel="shortcut icon" type="image/jng" 
href="/resources/images/favicon.png" />
-    <script src="/resources/js/global.js"></script>
-    <script src="/resources/js/functions.js"></script>
-    <link rel="stylesheet" type="text/css" href="/resources/css/screen.css" 
media="screen" />
+    <link rel="shortcut icon" type="image/jng" 
href="resources/images/favicon.png" />
+    <script src="resources/js/global.js"></script>
+    <script src="resources/js/functions.js"></script>
+    <link rel="stylesheet" type="text/css" href="resources/css/screen.css" 
media="screen" />
 
     <script>
       /**
@@ -64,10 +65,10 @@
       });
     </script>
     <#if js??>
-      <script src="/resources/js/${js}"></script>
+      <script src="resources/js/${js}"></script>
     </#if>
-    <script src="/resources/js/navbar.js"></script>
-    <script src="/resources/js/systemAlert.js"></script>
+    <script src="resources/js/navbar.js"></script>
+    <script src="resources/js/systemAlert.js"></script>
   </head>
 
   <body>
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/log.ftl
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/log.ftl
index cfa131cbc3..2c57623610 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/log.ftl
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/log.ftl
@@ -28,7 +28,7 @@
         $(document).ready(function() {
           logList = $('#logTable').DataTable( {
             "ajax": {
-              "url": '/rest/logs',
+              "url": '${rootContext}rest/logs',
               "dataSrc": ""
             },
             "stateSave": true,
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/modals.ftl
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/modals.ftl
index 471b5e9dd6..fb86006c4a 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/modals.ftl
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/modals.ftl
@@ -27,7 +27,7 @@
             <div class="modal-header justify-content-center">
               <span class="modal-title">
                 <div class="text-center">
-                  <a href="https://accumulo.apache.org"; target="_blank"><img 
alt="Apache Accumulo" src="/resources/images/accumulo-logo.png" /></a>
+                  <a href="https://accumulo.apache.org"; target="_blank"><img 
alt="Apache Accumulo" src="resources/images/accumulo-logo.png" /></a>
               </span>
             </div>
           </div>
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/navbar.ftl
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/navbar.ftl
index 5b325c2114..7e77c4c3fe 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/navbar.ftl
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/navbar.ftl
@@ -21,8 +21,8 @@
     <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
       <div class="container-fluid">
         <div class="navbar-header">
-          <a class="navbar-brand" id="headertitle" style="text-decoration: 
none" href="/">
-            <img id="accumulo-avatar" alt="accumulo" class="navbar-left" 
src="/resources/images/accumulo-avatar.png" />
+          <a class="navbar-brand" id="headertitle" style="text-decoration: 
none" href="${rootContext}">
+            <img id="accumulo-avatar" alt="accumulo" class="navbar-left" 
src="resources/images/accumulo-avatar.png" />
             ${instance_name}
           </a>
           <button class="navbar-toggler" type="button" 
data-bs-toggle="collapse" data-bs-target="#nav-items" aria-controls="nav-items" 
aria-expanded="false" aria-label="Toggle navigation">
@@ -39,24 +39,24 @@
                 <span id="statusNotification" class="icon-dot 
normal"></span>&nbspServers
               </a>
               <ul class="dropdown-menu">
-                <li><a class="dropdown-item" href="/manager"><span 
id="managerStatusNotification" class="icon-dot 
normal"></span>&nbsp;Manager&nbsp;Server&nbsp;</a></li>
-                <li><a class="dropdown-item" href="/tservers"><span 
id="serverStatusNotification" class="icon-dot 
normal"></span>&nbsp;Tablet&nbsp;Servers&nbsp;</a></li>
-                <li><a class="dropdown-item" href="/gc"><span 
id="gcStatusNotification" class="icon-dot 
normal"></span>&nbsp;Garbage&nbsp;collector&nbsp;</a></li>
+                <li><a class="dropdown-item" href="manager"><span 
id="managerStatusNotification" class="icon-dot 
normal"></span>&nbsp;Manager&nbsp;Server&nbsp;</a></li>
+                <li><a class="dropdown-item" href="tservers"><span 
id="serverStatusNotification" class="icon-dot 
normal"></span>&nbsp;Tablet&nbsp;Servers&nbsp;</a></li>
+                <li><a class="dropdown-item" href="gc"><span 
id="gcStatusNotification" class="icon-dot 
normal"></span>&nbsp;Garbage&nbsp;collector&nbsp;</a></li>
               </ul>
             </li>
             <li>
-              <a class="nav-link" aria-current="page" href="/tables">Tables</a>
+              <a class="nav-link" aria-current="page" href="tables">Tables</a>
             </li>
             <li class="dropdown">
               <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" 
role="button" data-bs-toggle="dropdown" aria-expanded="false">
                 Activity
               </a>
               <ul class="dropdown-menu col-xs-12" 
aria-labelledby="navbarDropdown">
-                <li><a class="dropdown-item" 
href="/compactions">Active&nbsp;Compactions</a></li>
-                <li><a class="dropdown-item" 
href="/scans">Active&nbsp;Scans</a></li>
-                <li><a class="dropdown-item" 
href="/bulkImports">Bulk&nbsp;Imports</a></li>
-                <li><a class="dropdown-item" 
href="/ec">External&nbsp;Compactions</a></li>
-                <li><a class="dropdown-item" 
href="/replication">Replication</a></li>
+                <li><a class="dropdown-item" 
href="compactions">Active&nbsp;Compactions</a></li>
+                <li><a class="dropdown-item" 
href="scans">Active&nbsp;Scans</a></li>
+                <li><a class="dropdown-item" 
href="bulkImports">Bulk&nbsp;Imports</a></li>
+                <li><a class="dropdown-item" 
href="ec">External&nbsp;Compactions</a></li>
+                <li><a class="dropdown-item" 
href="replication">Replication</a></li>
               </ul>
             </li>
             <li class="dropdown">
@@ -64,8 +64,8 @@
               role="button" data-bs-toggle="dropdown" 
aria-expanded="false">Debug&nbsp;<span id="errorsNotification" 
class="badge"></span><span class="caret"></span>
               </a>
               <ul class="dropdown-menu">
-                <li><a class="dropdown-item" 
href="/log">Recent&nbsp;Logs&nbsp;<span id="recentLogsNotifications" 
class="badge"></span></a></li>
-                <li><a class="dropdown-item" 
href="/problems">Table&nbsp;Problems&nbsp;<span id="tableProblemsNotifications" 
class="badge"></span></a></li>
+                <li><a class="dropdown-item" 
href="log">Recent&nbsp;Logs&nbsp;<span id="recentLogsNotifications" 
class="badge"></span></a></li>
+                <li><a class="dropdown-item" 
href="problems">Table&nbsp;Problems&nbsp;<span id="tableProblemsNotifications" 
class="badge"></span></a></li>
               </ul>
             </li>
             <li class="dropdown">
@@ -73,8 +73,8 @@
               role="button" data-bs-toggle="dropdown" 
aria-expanded="false">REST
               </a>
               <ul class="dropdown-menu dropdown-menu-end">
-                <li><a class="dropdown-item" href="/rest/xml">XML 
Summary</a></li>
-                <li><a class="dropdown-item" href="/rest/json">JSON 
Summary</a></li>
+                <li><a class="dropdown-item" href="rest/xml">XML 
Summary</a></li>
+                <li><a class="dropdown-item" href="rest/json">JSON 
Summary</a></li>
               </ul>
             </li>
             <li class="dropdown">
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/overview.ftl
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/overview.ftl
index 1edcc570a6..70585ebdbf 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/overview.ftl
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/overview.ftl
@@ -28,21 +28,21 @@
           <table class="table table-bordered table-striped table-condensed">
             <thead>
               <tr>
-                <th colspan="2"><a href="/manager">Accumulo Manager</a></th>
+                <th colspan="2"><a href="manager">Accumulo Manager</a></th>
               </tr>
               <tr>
                 <td colspan="2" class="center" style="display:none;"><span 
class="label label-danger nowrap">Manager is Down</span></td>
               </tr>
               <tr>
-                <td class="left"><a href="/tables">Tables</a></td>
+                <td class="left"><a href="tables">Tables</a></td>
                 <td class="right"></td>
               </tr>
               <tr>
-                <td class="left"><a 
href="/tservers">Total&nbsp;Known&nbsp;Tablet&nbsp;Servers</a></td>
+                <td class="left"><a 
href="tservers">Total&nbsp;Known&nbsp;Tablet&nbsp;Servers</a></td>
                 <td class="right"></td>
               </tr>
               <tr>
-                <td class="left"><a 
href="/tservers">Dead&nbsp;Tablet&nbsp;Servers</a></td>
+                <td class="left"><a 
href="tservers">Dead&nbsp;Tablet&nbsp;Servers</a></td>
                 <td class="right"></td>
               </tr>
               <tr>
diff --git 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/tables.ftl
 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/tables.ftl
index da1351aacc..c010c68dae 100644
--- 
a/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/tables.ftl
+++ 
b/server/monitor/src/main/resources/org/apache/accumulo/monitor/templates/tables.ftl
@@ -29,7 +29,7 @@
 
           tableList = $('#tableList').DataTable({
             "ajax": {
-              "url": "/rest/tables",
+              "url": '${rootContext}rest/tables',
               "dataSrc": "table"
             },
             "stateSave": true,
@@ -79,7 +79,7 @@
                 "type": "html",
                 "render": function (data, type, row, meta) {
                   if (type === 'display') {
-                    data = '<a href="/tables/' + row.tableId + '">' + 
row.tablename + '</a>';
+                    data = '<a href="tables/' + row.tableId + '">' + 
row.tablename + '</a>';
                   }
                   return data;
                 }
diff --git 
a/server/monitor/src/test/java/org/apache/accumulo/monitor/EmbeddedWebServerTest.java
 
b/server/monitor/src/test/java/org/apache/accumulo/monitor/EmbeddedWebServerTest.java
new file mode 100644
index 0000000000..1b1cd90b0c
--- /dev/null
+++ 
b/server/monitor/src/test/java/org/apache/accumulo/monitor/EmbeddedWebServerTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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
+ *
+ *   https://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.accumulo.monitor;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.accumulo.core.conf.ConfigurationCopy;
+import org.apache.accumulo.core.conf.DefaultConfiguration;
+import org.apache.accumulo.core.conf.Property;
+import org.apache.accumulo.server.ServerContext;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Basic tests for EmbeddedWebServer
+ */
+public class EmbeddedWebServerTest {
+
+  private static final AtomicReference<Monitor> monitor = new 
AtomicReference<>(null);
+
+  private static final AtomicReference<ConfigurationCopy> configuration = new 
AtomicReference<>();
+
+  @BeforeAll
+  public static void createMocks() {
+
+    // Mock a configuration with the new context Path
+    ConfigurationCopy config = new 
ConfigurationCopy(DefaultConfiguration.getInstance());
+    config.set(Property.MONITOR_ROOT_CONTEXT, "/test/");
+    configuration.set(config);
+
+    ServerContext contextMock = createMock(ServerContext.class);
+    expect(contextMock.getConfiguration()).andReturn(config).atLeastOnce();
+
+    Monitor monitorMock = createMock(Monitor.class);
+    expect(monitorMock.getContext()).andReturn(contextMock).atLeastOnce();
+    expect(monitorMock.getConfiguration()).andReturn(config).atLeastOnce();
+    
expect(monitorMock.getBindAddress()).andReturn("localhost:9995").atLeastOnce();
+
+    replay(contextMock, monitorMock);
+    monitor.set(monitorMock);
+  }
+
+  @AfterAll
+  public static void finishMocks() {
+    Monitor m = monitor.get();
+    verify(m.getContext(), m);
+  }
+
+  @Test
+  public void testContextPath() {
+    // Test removal of trailing slash
+    EmbeddedWebServer ews = new EmbeddedWebServer(monitor.get(),
+        Integer.parseInt(Property.MONITOR_PORT.getDefaultValue()));
+    assertEquals("/test", ews.getContextPath(),
+        "Context path of " + ews.getContextPath() + " does not match");
+    // Test redirect URL
+    configuration.get().set(Property.MONITOR_ROOT_CONTEXT, "/../test");
+    IllegalArgumentException exception =
+        assertThrows(IllegalArgumentException.class, () -> new 
EmbeddedWebServer(monitor.get(),
+            Integer.parseInt(Property.MONITOR_PORT.getDefaultValue())));
+    assertEquals("Root context: \"/../test\" is not a valid URL", 
exception.getMessage());
+    // Test whitespace in URL
+    configuration.get().set(Property.MONITOR_ROOT_CONTEXT, "/whitespace 
/test");
+    exception =
+        assertThrows(IllegalArgumentException.class, () -> new 
EmbeddedWebServer(monitor.get(),
+            Integer.parseInt(Property.MONITOR_PORT.getDefaultValue())));
+    assertEquals("Root context: \"/whitespace /test\" is not a valid URL", 
exception.getMessage());
+  }
+}

Reply via email to