Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/***************************** BEGIN LICENSE BLOCK ***************************

The contents of this file are subject to the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file, You can obtain one
at http://mozilla.org/MPL/2.0/.

Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the License.

The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial
Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved.

******************************* END LICENSE BLOCK ***************************/

package org.sensorhub.impl.service;

import org.sensorhub.api.config.DisplayInfo;

public class FileServerConfig {

@DisplayInfo(desc="Directory where static web content is located.")
public String staticContentRootUrl;

@DisplayInfo(desc="Root URL where static web content will be served.")
public String staticContentRootDir;

@DisplayInfo(label="Require Authentication", desc="Set to require remote users to be authentified before they can use this service")
public boolean requireAuth;

public FileServerConfig() {}

public FileServerConfig(String contentUrl, String contentDir, boolean requireAuth) {
this.requireAuth = requireAuth;
this.staticContentRootDir = contentDir;
this.staticContentRootUrl = contentUrl;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.HashMap;

import javax.servlet.DispatcherType;
import javax.servlet.ServletException;
Expand Down Expand Up @@ -161,26 +162,6 @@ protected synchronized void doStart() throws SensorHubException
server.addConnector(https);
}

// static content
ContextHandler fileResourceContext = null;
if (config.staticDocsRootUrl != null)
{
ResourceHandler fileResourceHandler = new ResourceHandler();
fileResourceHandler.setEtags(true);

fileResourceContext = new ContextHandler();
fileResourceContext.setContextPath(config.staticDocsRootUrl);
//fileResourceContext.setAllowNullPathInfo(true);
fileResourceContext.setHandler(fileResourceHandler);
fileResourceContext.setResourceBase(config.staticDocsRootDir);

//fileResourceContext.clearAliasChecks();
//fileResourceContext.addAliasCheck(new SymlinkAllowedResourceAliasChecker(fileResourceContext));

handlers.addHandler(fileResourceContext);
getLogger().info("Static resources root is " + config.staticDocsRootUrl);
}

// servlets
if (config.servletsRootUrl != null)
{
Expand All @@ -189,16 +170,16 @@ protected synchronized void doStart() throws SensorHubException
servletHandler.setContextPath(config.servletsRootUrl);
handlers.addHandler(servletHandler);
getLogger().info("Servlets root is " + config.servletsRootUrl);

// security handler
if (config.authMethod != null && config.authMethod != AuthMethod.NONE)
{
jettySecurityHandler = new ConstraintSecurityHandler();

// create login service connected to OSH security manager
ISecurityManager securityManager = getParentHub().getSecurityManager();
OshLoginService loginService = new OshLoginService(securityManager);

if (config.authMethod == AuthMethod.BASIC)
jettySecurityHandler.setAuthenticator(new HttpLogoutWrapper(new BasicAuthenticator(), getLogger()));
else if (config.authMethod == AuthMethod.DIGEST)
Expand All @@ -212,11 +193,11 @@ else if (config.authMethod == AuthMethod.EXTERNAL)
throw new IllegalStateException("External authentication method was selected but no authenticator implementation is available");
jettySecurityHandler.setAuthenticator(authenticator);
}

jettySecurityHandler.setLoginService(loginService);
servletHandler.setSecurityHandler(jettySecurityHandler);
}

// filter to add proper cross-origin headers
if (config.enableCORS)
{
Expand Down Expand Up @@ -252,6 +233,38 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se
}),"/test");
addServletSecurity("/test", false);
}

Map<String, ContextHandler> fileResourceContexts = new HashMap<>();
for (FileServerConfig fileServerConfig : config.fileServerConfigs) {

// static content
ContextHandler fileResourceContext = null;
if (fileServerConfig.staticContentRootUrl != null) {
ResourceHandler fileResourceHandler = new ResourceHandler();
fileResourceHandler.setEtags(true);

fileResourceContext = new ContextHandler();
fileResourceContext.setContextPath(fileServerConfig.staticContentRootUrl);
fileResourceContext.setHandler(fileResourceHandler);
fileResourceContext.setResourceBase(fileServerConfig.staticContentRootDir);

// Add authentication to file server if configured
if (fileServerConfig.requireAuth && jettySecurityHandler != null) {
ConstraintSecurityHandler fileServerSecurityHandler = new ConstraintSecurityHandler();
fileServerSecurityHandler.setAuthenticator(jettySecurityHandler.getAuthenticator());
fileServerSecurityHandler.setLoginService(jettySecurityHandler.getLoginService());
fileServerSecurityHandler.setHandler(fileResourceContext);
addServletSecurity(fileServerSecurityHandler, fileServerConfig.staticContentRootUrl, fileServerConfig.requireAuth, Constraint.ANY_AUTH);
handlers.addHandler(fileServerSecurityHandler);
} else {
handlers.addHandler(fileResourceContext);
}

fileResourceContexts.put(fileServerConfig.staticContentRootUrl, fileResourceContext);

getLogger().info("Static resource root served at " + fileServerConfig.staticContentRootUrl);
}
}

server.setHandler(handlers);

Expand All @@ -270,8 +283,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se
xmlConfig.getIdMap().put(OSH_HTTP_CONNECTOR_ID, http);
if (https != null)
xmlConfig.getIdMap().put(OSH_HTTPS_CONNECTOR_ID, https);
if (fileResourceContext != null)
xmlConfig.getIdMap().put(OSH_STATIC_CONTENT_ID, fileResourceContext);
for (Map.Entry<String, ContextHandler> entry : fileResourceContexts.entrySet())
xmlConfig.getIdMap().put(OSH_STATIC_CONTENT_ID + "-" + entry.getKey(), entry.getValue());
if (servletHandler != null)
xmlConfig.getIdMap().put(OSH_SERVLET_HANDLER_ID, servletHandler);

Expand Down Expand Up @@ -400,7 +413,11 @@ public void addServletSecurity(String pathSpec, boolean requireAuth)

public synchronized void addServletSecurity(String pathSpec, boolean requireAuth, String... roles)
{
if (jettySecurityHandler != null)
addServletSecurity(jettySecurityHandler, pathSpec, requireAuth, roles);
}

public synchronized void addServletSecurity(ConstraintSecurityHandler securityHandler, String pathSpec, boolean requireAuth, String... roles) {
if (securityHandler != null)
{
Constraint constraint = new Constraint();
constraint.setRoles(roles);
Expand All @@ -409,7 +426,7 @@ public synchronized void addServletSecurity(String pathSpec, boolean requireAuth
cm.setConstraint(constraint);
cm.setPathSpec(pathSpec);
cm.setMethodOmissions(SECURITY_EXCLUDED_METHODS); // disable auth on OPTIONS requests (needed for CORS)
jettySecurityHandler.addConstraintMapping(cm);
securityHandler.addConstraintMapping(cm);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
import org.sensorhub.api.config.DisplayInfo.FieldType.Type;
import org.sensorhub.api.module.ModuleConfig;

import java.util.ArrayList;
import java.util.List;


/**
* <p>
Expand All @@ -42,14 +45,13 @@ public enum AuthMethod
@DisplayInfo(label="HTTPS Port", desc="TCP port where server will listen for secure HTTP (HTTPS) connections (use 0 to disable HTTPS).")
public int httpsPort = 0;


@DisplayInfo(desc="Root URL where static web content will be served.")
public String staticDocsRootUrl = "/";


@DisplayInfo(desc="Directory where static web content is located.")
public String staticDocsRootDir = "web";


@DisplayInfo(label = "Static File Servers", desc = "Static web content configurations")
public List<FileServerConfig> fileServerConfigs = new ArrayList<>(List.of(new FileServerConfig(
"/",
"web",
false)));


@DisplayInfo(desc="Root URL where the server will accept requests. This will be the prefix to all servlet URLs.")
public String servletsRootUrl = "/sensorhub";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Authenticator;
import java.net.HttpURLConnection;
import java.net.PasswordAuthentication;
import java.net.URL;
import java.util.ArrayList;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
Expand All @@ -33,13 +35,17 @@
import org.sensorhub.impl.security.BasicSecurityRealmConfig;
import org.sensorhub.impl.security.BasicSecurityRealmConfig.UserConfig;
import org.sensorhub.impl.service.HttpServerConfig.AuthMethod;
import org.vast.util.Asserts;


public class TestHttpServer
{
private static String USER_ID = "admin";
private static String PASSWORD = "pwd";

private static final String USER_ID = "admin";
private static final String PASSWORD = "pwd";
private static final String PRIVATE_PATH = "/testStaticPrivate";
private static final String PUBLIC_PATH = "/testStaticPublic";
private static final String TEST_FILE = "/test.txt";

ModuleRegistry registry;


Expand All @@ -49,13 +55,29 @@ public void setup() throws Exception
System.out.println("\n*****************************");
var hub = new SensorHub();
hub.start();
registry = hub.getModuleRegistry();
registry = hub.getModuleRegistry();
Authenticator.setDefault(null);
}


private HttpServer startServer(AuthMethod authMethod) throws Exception
{
HttpServerConfig config = new HttpServerConfig();

var testPublicFilepath = TestHttpServer.class.getResource(PUBLIC_PATH).getFile();
var testPrivateFilepath = TestHttpServer.class.getResource(PRIVATE_PATH).getFile();

config.fileServerConfigs = new ArrayList<>();
config.fileServerConfigs.add(new FileServerConfig(
PUBLIC_PATH,
testPublicFilepath,
false
));
config.fileServerConfigs.add(new FileServerConfig(
PRIVATE_PATH + "/*",
testPrivateFilepath,
true
));
config.autoStart = true;
config.authMethod = authMethod;
return (HttpServer)registry.loadModule(config);
Expand All @@ -72,7 +94,50 @@ private void addUsers() throws Exception
securityConfig.users.add(user);
registry.loadModule(securityConfig);
}


private void testConnectStatus(AuthMethod authMethod, boolean isAuthenticated, String endpoint, int expectedValue) throws Exception {
addUsers();
var httpServer = startServer(authMethod);

// connect to servlet and check response
URL url = new URL(httpServer.getServerBaseUrl() + endpoint);
System.out.println(url);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();

if (authMethod != null && isAuthenticated)
{
conn.setAuthenticator(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
PasswordAuthentication pa = new PasswordAuthentication (USER_ID, PASSWORD.toCharArray());
return pa;
}
});
}

conn.setRequestMethod("GET");
conn.setUseCaches(false);
conn.connect();

// System.out.println(conn.getResponseMessage());
// System.out.println(conn.getResponseCode());
Asserts.checkArgument(conn.getResponseCode() == expectedValue);
}

@Test
public void testConnectPublicContent() throws Exception {
testConnectStatus(AuthMethod.NONE, false, PUBLIC_PATH, 200);
}

@Test
public void testConnectPrivateContentWithAuth() throws Exception {
testConnectStatus(AuthMethod.BASIC, true, PRIVATE_PATH + TEST_FILE, 200);
}

@Test
public void testConnectPrivateContentNoAuth() throws Exception {
testConnectStatus(AuthMethod.BASIC, false, PRIVATE_PATH + TEST_FILE, 401);
}

@Test
public void testStartServer() throws Exception
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
im private
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
im public
Loading