mirror of
https://github.com/Telecominfraproject/wlan-cloud-rrm.git
synced 2025-10-29 09:42:22 +00:00
[WIFI-10736] Spin up separate ports for internal and external (#64)
Signed-off-by: Jun Woo Shin <jwoos@fb.com>
This commit is contained in:
@@ -11,7 +11,7 @@ RUN mkdir /owrrm-data
|
||||
WORKDIR /usr/src/java
|
||||
COPY docker-entrypoint.sh /
|
||||
COPY --from=build /usr/src/java/owrrm/target/openwifi-rrm.jar /usr/local/bin/
|
||||
EXPOSE 16789
|
||||
EXPOSE 16789 16790
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
CMD ["java", "-XX:+IdleTuningGcOnIdle", "-Xtune:virtualized", \
|
||||
"-jar", "/usr/local/bin/openwifi-rrm.jar", \
|
||||
|
||||
@@ -313,7 +313,11 @@ public class UCentralClient {
|
||||
Map<String, Object> parameters
|
||||
) {
|
||||
return httpGet(
|
||||
endpoint, service, parameters, connectTimeoutMs, socketTimeoutMs
|
||||
endpoint,
|
||||
service,
|
||||
parameters,
|
||||
connectTimeoutMs,
|
||||
socketTimeoutMs
|
||||
);
|
||||
}
|
||||
|
||||
@@ -353,7 +357,11 @@ public class UCentralClient {
|
||||
Object body
|
||||
) {
|
||||
return httpPost(
|
||||
endpoint, service, body, connectTimeoutMs, socketTimeoutMs
|
||||
endpoint,
|
||||
service,
|
||||
body,
|
||||
connectTimeoutMs,
|
||||
socketTimeoutMs
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ public class WebTokenResult {
|
||||
public String access_token;
|
||||
public String refresh_token;
|
||||
public String token_type;
|
||||
public int expires_in;
|
||||
public long expires_in;
|
||||
public int idle_timeout;
|
||||
public String username;
|
||||
public long created;
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.openwifi.rrm;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.ForwardedRequestCustomizer;
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.HttpConnectionFactory;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
||||
import org.eclipse.jetty.util.thread.ThreadPool;
|
||||
import spark.embeddedserver.jetty.JettyServerFactory;
|
||||
import spark.utils.Assert;
|
||||
|
||||
/**
|
||||
* Creates Jetty Server instances. Majority of the logic is taken from
|
||||
* JettyServerFactory. The additional feature is that this class will actually
|
||||
* set two connectors (original class doesn't set any connectors at all and
|
||||
* leaves it up to the serivce start logic). Since we set two connectors here
|
||||
* on the server, Spark uses the existing conectors instead of trying to spin
|
||||
* up its own connectors. The other difference is that it uses a different
|
||||
* ServerConnector constructor to avoid allocating additional threads that
|
||||
* aren't necessary ({@link #makeConnector})
|
||||
* @see EmbeddedJettyFactory
|
||||
*/
|
||||
public class CustomJettyServerFactory implements JettyServerFactory {
|
||||
// normally this is set in EmbeddedJettyServer but since we create our own connectors here,
|
||||
// we need the value here
|
||||
private boolean trustForwardHeaders = true; // true by default
|
||||
private final int internalPort;
|
||||
private final int externalPort;
|
||||
|
||||
public CustomJettyServerFactory(int internalPort, int externalPort) {
|
||||
this.internalPort = internalPort;
|
||||
this.externalPort = externalPort;
|
||||
}
|
||||
|
||||
public void setTrustForwardHeaders(boolean trustForwardHeaders) {
|
||||
this.trustForwardHeaders = trustForwardHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is basically
|
||||
* spark.embeddedserver.jetty.SocketConnectorFactory.createSocketConnector,
|
||||
* the only difference being that we use a different constructor for the
|
||||
* Connector and that the private methods called are just inlined.
|
||||
*/
|
||||
public Connector makeConnector(
|
||||
Server server,
|
||||
String host,
|
||||
int port,
|
||||
boolean trustForwardHeaders
|
||||
) {
|
||||
Assert.notNull(server, "'server' must not be null");
|
||||
Assert.notNull(host, "'host' must not be null");
|
||||
|
||||
// spark.embeddedserver.jetty.SocketConnectorFactory.createHttpConnectionFactory
|
||||
HttpConfiguration httpConfig = new HttpConfiguration();
|
||||
httpConfig.setSecureScheme("https");
|
||||
if (trustForwardHeaders) {
|
||||
httpConfig.addCustomizer(new ForwardedRequestCustomizer());
|
||||
}
|
||||
HttpConnectionFactory httpConnectionFactory =
|
||||
new HttpConnectionFactory(httpConfig);
|
||||
|
||||
ServerConnector connector = new ServerConnector(
|
||||
server,
|
||||
0, // acceptors, don't allocate separate threads for acceptor
|
||||
0, // selectors, use default number
|
||||
httpConnectionFactory
|
||||
);
|
||||
// spark.embeddedserver.jetty.SocketConnectorFactory.initializeConnector
|
||||
connector.setIdleTimeout(TimeUnit.HOURS.toMillis(1));
|
||||
connector.setHost(host);
|
||||
connector.setPort(port);
|
||||
|
||||
return connector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Jetty server.
|
||||
*
|
||||
* @param maxThreads maxThreads
|
||||
* @param minThreads minThreads
|
||||
* @param threadTimeoutMillis threadTimeoutMillis
|
||||
* @return a new jetty server instance
|
||||
*/
|
||||
public Server create(
|
||||
int maxThreads,
|
||||
int minThreads,
|
||||
int threadTimeoutMillis
|
||||
) {
|
||||
Server server;
|
||||
|
||||
if (maxThreads > 0) {
|
||||
int max = maxThreads;
|
||||
int min = (minThreads > 0) ? minThreads : 8;
|
||||
int idleTimeout =
|
||||
(threadTimeoutMillis > 0) ? threadTimeoutMillis : 60000;
|
||||
|
||||
server = new Server(new QueuedThreadPool(max, min, idleTimeout));
|
||||
} else {
|
||||
server = new Server();
|
||||
}
|
||||
|
||||
Connector internalConnector = null;
|
||||
if (internalPort != -1) {
|
||||
internalConnector = makeConnector(
|
||||
server,
|
||||
"localhost",
|
||||
internalPort,
|
||||
trustForwardHeaders
|
||||
);
|
||||
}
|
||||
|
||||
Connector externalConnector = null;
|
||||
if (externalPort != -1) {
|
||||
externalConnector = makeConnector(
|
||||
server,
|
||||
"localhost",
|
||||
externalPort,
|
||||
trustForwardHeaders
|
||||
);
|
||||
}
|
||||
|
||||
if (internalConnector == null) {
|
||||
server.setConnectors(new Connector[] { externalConnector });
|
||||
} else if (externalConnector == null) {
|
||||
server.setConnectors(new Connector[] { internalConnector });
|
||||
} else {
|
||||
server.setConnectors(
|
||||
new Connector[] { internalConnector, externalConnector }
|
||||
);
|
||||
}
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Jetty server with supplied thread pool
|
||||
* @param threadPool thread pool
|
||||
* @return a new jetty server instance
|
||||
*/
|
||||
@Override
|
||||
public Server create(ThreadPool threadPool) {
|
||||
return threadPool != null ? new Server(threadPool) : new Server();
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ public class RRMConfig {
|
||||
* Private endpoint for the RRM service
|
||||
* ({@code SERVICECONFIG_PRIVATEENDPOINT})
|
||||
*/
|
||||
public String privateEndpoint = "http://owrrm.wlan.local:16789"; // see ApiServerParams.httpPort
|
||||
public String privateEndpoint = "http://owrrm.wlan.local:16790"; // see ApiServerParams.internalHttpPort
|
||||
|
||||
/**
|
||||
* Public endpoint for the RRM service
|
||||
@@ -325,10 +325,16 @@ public class RRMConfig {
|
||||
*/
|
||||
public class ApiServerParams {
|
||||
/**
|
||||
* The HTTP port to listen on, or -1 to disable
|
||||
* ({@code APISERVERPARAMS_HTTPPORT})
|
||||
* The HTTP port to listen on for internal traffic, or -1 to disable
|
||||
* ({@code APISERVERPARAMS_INTERNALHTTPPORT})
|
||||
*/
|
||||
public int httpPort = 16789;
|
||||
public int internalHttpPort = 16790;
|
||||
|
||||
/**
|
||||
* The HTTP port to listen on for external traffic, or -1 to disable
|
||||
* ({@code APISERVERPARAMS_EXTERNALHTTPPORT})
|
||||
*/
|
||||
public int externalHttpPort = 16789;
|
||||
|
||||
/**
|
||||
* Comma-separated list of all allowed CORS domains (exact match
|
||||
@@ -543,8 +549,11 @@ public class RRMConfig {
|
||||
}
|
||||
ModuleConfig.ApiServerParams apiServerParams =
|
||||
config.moduleConfig.apiServerParams;
|
||||
if ((v = env.get("APISERVERPARAMS_HTTPPORT")) != null) {
|
||||
apiServerParams.httpPort = Integer.parseInt(v);
|
||||
if ((v = env.get("APISERVERPARAMS_INTERNALHTTPPORT")) != null) {
|
||||
apiServerParams.internalHttpPort = Integer.parseInt(v);
|
||||
}
|
||||
if ((v = env.get("APISERVERPARAMS_EXTERNALHTTPPORT")) != null) {
|
||||
apiServerParams.externalHttpPort = Integer.parseInt(v);
|
||||
}
|
||||
if ((v = env.get("APISERVERPARAMS_CORSDOMAINLIST")) != null) {
|
||||
apiServerParams.corsDomainList = v;
|
||||
|
||||
@@ -40,6 +40,7 @@ import com.facebook.openwifi.cloudsdk.models.gw.SystemInfoResults;
|
||||
import com.facebook.openwifi.cloudsdk.models.gw.TokenValidationResult;
|
||||
import com.facebook.openwifi.cloudsdk.models.prov.rrm.Algorithm;
|
||||
import com.facebook.openwifi.cloudsdk.models.prov.rrm.Provider;
|
||||
import com.facebook.openwifi.rrm.CustomJettyServerFactory;
|
||||
import com.facebook.openwifi.rrm.DeviceConfig;
|
||||
import com.facebook.openwifi.rrm.DeviceDataManager;
|
||||
import com.facebook.openwifi.rrm.DeviceLayeredConfig;
|
||||
@@ -81,7 +82,9 @@ import io.swagger.v3.oas.models.OpenAPI;
|
||||
import spark.Request;
|
||||
import spark.Response;
|
||||
import spark.Route;
|
||||
import spark.Spark;
|
||||
import spark.Service;
|
||||
import spark.embeddedserver.EmbeddedServers;
|
||||
import spark.embeddedserver.jetty.EmbeddedJettyFactory;
|
||||
|
||||
/**
|
||||
* HTTP API server.
|
||||
@@ -110,6 +113,27 @@ public class ApiServer implements Runnable {
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(ApiServer.class);
|
||||
|
||||
/**
|
||||
* This is the identifier for the server factory that Spark should use. This
|
||||
* particular identifier points to the custom factory that we register to
|
||||
* enable running multiple ports on one service.
|
||||
*
|
||||
* @see #run()
|
||||
*/
|
||||
private static final String SPARK_EMBEDDED_SERVER_IDENTIFIER =
|
||||
ApiServer.class.getName();
|
||||
|
||||
/**
|
||||
* The Spark service instance. Normally, you would use the static methods on
|
||||
* Spark, but since we need to spin up multiple instances of Spark for testing,
|
||||
* we choose to go with instantiating the service ourselves. There is really no
|
||||
* difference except with the static method, Spark calls ignite and holds a
|
||||
* singleton instance for us.
|
||||
*
|
||||
* @see Spark
|
||||
*/
|
||||
private final Service service;
|
||||
|
||||
/** The module parameters. */
|
||||
private final ApiServerParams params;
|
||||
|
||||
@@ -164,6 +188,7 @@ public class ApiServer implements Runnable {
|
||||
UCentralClient client,
|
||||
RRMScheduler scheduler
|
||||
) {
|
||||
this.service = Service.ignite();
|
||||
this.params = params;
|
||||
this.serviceConfig = serviceConfig;
|
||||
this.serviceKey = Utils.generateServiceKey(serviceConfig);
|
||||
@@ -194,64 +219,116 @@ public class ApiServer implements Runnable {
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Block until initialization finishes. Just calls the method on the
|
||||
* underlying service.
|
||||
*/
|
||||
public void awaitInitialization() {
|
||||
service.awaitInitialization();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
this.startTimeMs = System.currentTimeMillis();
|
||||
|
||||
if (params.httpPort == -1) {
|
||||
if (params.internalHttpPort == -1 && params.externalHttpPort == -1) {
|
||||
logger.info("API server is disabled.");
|
||||
return;
|
||||
} else if (params.internalHttpPort == -1) {
|
||||
logger.info("Internal API server is disabled");
|
||||
} else if (params.externalHttpPort == -1) {
|
||||
logger.info("External API server is disabled");
|
||||
}
|
||||
|
||||
if (params.internalHttpPort == params.externalHttpPort) {
|
||||
logger.error(
|
||||
"Internal and external port cannot be the same - not starting API server"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Spark.port(params.httpPort);
|
||||
EmbeddedServers.add(
|
||||
SPARK_EMBEDDED_SERVER_IDENTIFIER,
|
||||
new EmbeddedJettyFactory(
|
||||
new CustomJettyServerFactory(
|
||||
params.internalHttpPort,
|
||||
params.externalHttpPort
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// use the embedded server factory added above, this is required so that we
|
||||
// don't mess up the default factory which can and will be used for
|
||||
// additional Spark services in testing
|
||||
service.embeddedServerIdentifier(SPARK_EMBEDDED_SERVER_IDENTIFIER);
|
||||
|
||||
// Usually you would call this with an actual port and Spark would spin up a
|
||||
// port on it. However, since we're putting our own connectors in so that we
|
||||
// can use two ports and Spark has logic to use connectors that already exist
|
||||
// so it doesn't matter what port we pass in here as long as it's not one of
|
||||
// the actual ports we're using (Spark has some weird logic where it still
|
||||
// tries to bind to the port).
|
||||
// @see EmbeddedJettyServer
|
||||
service.port(0);
|
||||
|
||||
// Configure API docs hosting
|
||||
Spark.staticFiles.location("/public");
|
||||
Spark.get("/openapi.yaml", this::getOpenApiYaml);
|
||||
Spark.get("/openapi.json", this::getOpenApiJson);
|
||||
service.staticFiles.location("/public");
|
||||
service.get("/openapi.yaml", this::getOpenApiYaml);
|
||||
service.get("/openapi.json", this::getOpenApiJson);
|
||||
|
||||
// Install routes
|
||||
Spark.before(this::beforeFilter);
|
||||
Spark.after(this::afterFilter);
|
||||
Spark.options("/*", this::options);
|
||||
Spark.get("/api/v1/system", new SystemEndpoint());
|
||||
Spark.post("/api/v1/system", new SetSystemEndpoint());
|
||||
Spark.get("/api/v1/provider", new ProviderEndpoint());
|
||||
Spark.get("/api/v1/algorithms", new AlgorithmsEndpoint());
|
||||
Spark.put("/api/v1/runRRM", new RunRRMEndpoint());
|
||||
Spark.get("/api/v1/getTopology", new GetTopologyEndpoint());
|
||||
Spark.post("/api/v1/setTopology", new SetTopologyEndpoint());
|
||||
Spark.get(
|
||||
service.before(this::beforeFilter);
|
||||
service.after(this::afterFilter);
|
||||
service.options("/*", this::options);
|
||||
service.get("/api/v1/system", new SystemEndpoint());
|
||||
service.post("/api/v1/system", new SetSystemEndpoint());
|
||||
service.get("/api/v1/provider", new ProviderEndpoint());
|
||||
service.get("/api/v1/algorithms", new AlgorithmsEndpoint());
|
||||
service.put("/api/v1/runRRM", new RunRRMEndpoint());
|
||||
service.get("/api/v1/getTopology", new GetTopologyEndpoint());
|
||||
service.post("/api/v1/setTopology", new SetTopologyEndpoint());
|
||||
service.get(
|
||||
"/api/v1/getDeviceLayeredConfig",
|
||||
new GetDeviceLayeredConfigEndpoint()
|
||||
);
|
||||
Spark.get("/api/v1/getDeviceConfig", new GetDeviceConfigEndpoint());
|
||||
Spark.post(
|
||||
service.get("/api/v1/getDeviceConfig", new GetDeviceConfigEndpoint());
|
||||
service.post(
|
||||
"/api/v1/setDeviceNetworkConfig",
|
||||
new SetDeviceNetworkConfigEndpoint()
|
||||
);
|
||||
Spark.post(
|
||||
service.post(
|
||||
"/api/v1/setDeviceZoneConfig",
|
||||
new SetDeviceZoneConfigEndpoint()
|
||||
);
|
||||
Spark.post(
|
||||
service.post(
|
||||
"/api/v1/setDeviceApConfig",
|
||||
new SetDeviceApConfigEndpoint()
|
||||
);
|
||||
Spark.post(
|
||||
service.post(
|
||||
"/api/v1/modifyDeviceApConfig",
|
||||
new ModifyDeviceApConfigEndpoint()
|
||||
);
|
||||
Spark.get("/api/v1/currentModel", new GetCurrentModelEndpoint());
|
||||
Spark.get("/api/v1/optimizeChannel", new OptimizeChannelEndpoint());
|
||||
Spark.get("/api/v1/optimizeTxPower", new OptimizeTxPowerEndpoint());
|
||||
service.get("/api/v1/currentModel", new GetCurrentModelEndpoint());
|
||||
service.get("/api/v1/optimizeChannel", new OptimizeChannelEndpoint());
|
||||
service.get("/api/v1/optimizeTxPower", new OptimizeTxPowerEndpoint());
|
||||
|
||||
logger.info("API server listening on HTTP port {}", params.httpPort);
|
||||
logger.info(
|
||||
"API server listening for HTTP internal on port {} and external on port {}",
|
||||
params.internalHttpPort,
|
||||
params.externalHttpPort
|
||||
);
|
||||
}
|
||||
|
||||
/** Stop the server. */
|
||||
public void shutdown() {
|
||||
Spark.stop();
|
||||
service.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Block until stop finishes. Just calls the method on the underlying service.
|
||||
*/
|
||||
public void awaitStop() {
|
||||
service.awaitStop();
|
||||
}
|
||||
|
||||
/** Reconstructs a URL. */
|
||||
@@ -269,15 +346,17 @@ public class ApiServer implements Runnable {
|
||||
* HTTP 403 response and return false.
|
||||
*/
|
||||
private boolean performOpenWifiAuth(Request request, Response response) {
|
||||
// TODO check if request came from internal endpoint
|
||||
boolean internal = true;
|
||||
String internalName = request.headers("X-INTERNAL-NAME");
|
||||
if (internal && internalName != null) {
|
||||
// Internal request, validate "X-API-KEY"
|
||||
String apiKey = request.headers("X-API-KEY");
|
||||
if (apiKey != null && apiKey.equals(serviceKey)) {
|
||||
// auth success
|
||||
return true;
|
||||
int port = request.port();
|
||||
boolean internal = port > 0 && port == params.internalHttpPort;
|
||||
if (internal) {
|
||||
String internalName = request.headers("X-INTERNAL-NAME");
|
||||
if (internalName != null) {
|
||||
// Internal request, validate "X-API-KEY"
|
||||
String apiKey = request.headers("X-API-KEY");
|
||||
if (apiKey != null && apiKey.equals(serviceKey)) {
|
||||
// auth success
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// External request, validate token:
|
||||
@@ -297,7 +376,7 @@ public class ApiServer implements Runnable {
|
||||
}
|
||||
|
||||
// auth failure
|
||||
Spark.halt(403, "Forbidden");
|
||||
service.halt(403, "Forbidden");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -325,10 +404,11 @@ public class ApiServer implements Runnable {
|
||||
private void beforeFilter(Request request, Response response) {
|
||||
// Log requests
|
||||
logger.debug(
|
||||
"[{}] {} {}",
|
||||
"[{}] {} {} on port {}",
|
||||
request.ip(),
|
||||
request.requestMethod(),
|
||||
getFullUrl(request.pathInfo(), request.queryString())
|
||||
getFullUrl(request.pathInfo(), request.queryString()),
|
||||
request.port()
|
||||
);
|
||||
|
||||
// Remove "Server: Jetty" header
|
||||
|
||||
@@ -24,10 +24,12 @@ import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInfo;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
import com.facebook.openwifi.cloudsdk.models.gw.ServiceEvent;
|
||||
import com.facebook.openwifi.cloudsdk.UCentralClient;
|
||||
import com.facebook.openwifi.cloudsdk.UCentralConstants;
|
||||
import com.facebook.openwifi.cloudsdk.kafka.UCentralKafkaConsumer;
|
||||
@@ -39,18 +41,33 @@ import com.facebook.openwifi.rrm.RRMAlgorithm;
|
||||
import com.facebook.openwifi.rrm.RRMConfig;
|
||||
import com.facebook.openwifi.rrm.VersionProvider;
|
||||
import com.facebook.openwifi.rrm.mysql.DatabaseManager;
|
||||
import com.facebook.openwifi.rrm.services.MockOWSecService;
|
||||
import com.facebook.openwifi.rrm.Utils;
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import kong.unirest.HttpResponse;
|
||||
import kong.unirest.JsonNode;
|
||||
import kong.unirest.Unirest;
|
||||
import kong.unirest.json.JSONObject;
|
||||
import spark.Spark;
|
||||
|
||||
/**
|
||||
* A class for testing ApiServer. In order to test auth logic, you must tag
|
||||
* the test "auth". Doing so will spin up a mock ow security service and
|
||||
* enable auth on the server.
|
||||
*/
|
||||
@TestMethodOrder(OrderAnnotation.class)
|
||||
public class ApiServerTest {
|
||||
/** The test server port. */
|
||||
private static final int TEST_PORT = spark.Service.SPARK_DEFAULT_PORT;
|
||||
/** The test server port for external calls. */
|
||||
private static final int TEST_EXTERNAL_PORT =
|
||||
spark.Service.SPARK_DEFAULT_PORT;
|
||||
|
||||
/** The test server port for internal calls. */
|
||||
private static final int TEST_INTERNAL_PORT =
|
||||
TEST_EXTERNAL_PORT + 1;
|
||||
|
||||
/** The mock ow sec service port */
|
||||
private static final int TEST_OWSEC_PORT =
|
||||
TEST_INTERNAL_PORT + 1;
|
||||
|
||||
/** Test device data manager. */
|
||||
private DeviceDataManager deviceDataManager;
|
||||
@@ -61,15 +78,27 @@ public class ApiServerTest {
|
||||
/** Test API server instance. */
|
||||
private ApiServer server;
|
||||
|
||||
/** Mock OW sec service */
|
||||
private MockOWSecService owSecService;
|
||||
|
||||
/** Test modeler instance. */
|
||||
private Modeler modeler;
|
||||
|
||||
/** The Gson instance. */
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
/** Build an endpoint URL. */
|
||||
/** Build an internal endpoint URL. */
|
||||
private String endpoint(String path) {
|
||||
return String.format("http://localhost:%d%s", TEST_PORT, path);
|
||||
return endpoint(path, true);
|
||||
}
|
||||
|
||||
/** Build an endpoint URL. */
|
||||
private String endpoint(String path, boolean internal) {
|
||||
return String.format(
|
||||
"http://localhost:%d%s",
|
||||
internal ? TEST_INTERNAL_PORT : TEST_EXTERNAL_PORT,
|
||||
path
|
||||
);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
@@ -78,10 +107,43 @@ public class ApiServerTest {
|
||||
|
||||
// Create config
|
||||
this.rrmConfig = new RRMConfig();
|
||||
rrmConfig.moduleConfig.apiServerParams.httpPort = TEST_PORT;
|
||||
rrmConfig.moduleConfig.apiServerParams.internalHttpPort =
|
||||
TEST_INTERNAL_PORT;
|
||||
rrmConfig.moduleConfig.apiServerParams.externalHttpPort =
|
||||
TEST_EXTERNAL_PORT;
|
||||
|
||||
UCentralClient client = null;
|
||||
|
||||
boolean useAuth = testInfo.getTags().contains("auth");
|
||||
if (useAuth) {
|
||||
this.rrmConfig.moduleConfig.apiServerParams.useOpenWifiAuth = true;
|
||||
|
||||
// spin up mock owsec service
|
||||
try {
|
||||
owSecService = new MockOWSecService(TEST_OWSEC_PORT);
|
||||
} catch (Exception e) {
|
||||
fail("Could not instantiate OWSec.", e);
|
||||
}
|
||||
|
||||
// Create uCentral client with mock services as necessary
|
||||
client = new UCentralClient(
|
||||
"rrm",
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
rrmConfig.uCentralConfig.uCentralSocketParams.connectTimeoutMs,
|
||||
rrmConfig.uCentralConfig.uCentralSocketParams.socketTimeoutMs,
|
||||
rrmConfig.uCentralConfig.uCentralSocketParams.wifiScanTimeoutMs
|
||||
);
|
||||
ServiceEvent owsecServiceEvent = new ServiceEvent();
|
||||
owsecServiceEvent.type = "owsec";
|
||||
owsecServiceEvent.privateEndPoint =
|
||||
String.format("http://localhost:%d", owSecService.getPort());
|
||||
client.setServiceEndpoint("owsec", owsecServiceEvent);
|
||||
}
|
||||
|
||||
// Create clients (null for now)
|
||||
UCentralClient client = null;
|
||||
UCentralKafkaConsumer consumer = null;
|
||||
DatabaseManager dbManager = null;
|
||||
|
||||
@@ -126,7 +188,7 @@ public class ApiServerTest {
|
||||
);
|
||||
try {
|
||||
server.run();
|
||||
Spark.awaitInitialization();
|
||||
server.awaitInitialization();
|
||||
} catch (Exception e) {
|
||||
fail("Could not instantiate ApiServer.", e);
|
||||
}
|
||||
@@ -137,10 +199,16 @@ public class ApiServerTest {
|
||||
// Destroy ApiServer
|
||||
if (server != null) {
|
||||
server.shutdown();
|
||||
Spark.awaitStop();
|
||||
server.awaitStop();
|
||||
}
|
||||
server = null;
|
||||
|
||||
// reset owsec server
|
||||
if (owSecService != null) {
|
||||
owSecService.stop();
|
||||
}
|
||||
owSecService = null;
|
||||
|
||||
// Reset Unirest client
|
||||
// Without this, Unirest randomly throws:
|
||||
// kong.unirest.UnirestException: java.net.SocketException: Software caused connection abort: recv failed
|
||||
@@ -511,7 +579,10 @@ public class ApiServerTest {
|
||||
@Order(1000)
|
||||
void testDocs() throws Exception {
|
||||
// Index page paths
|
||||
assertEquals(200, Unirest.get(endpoint("/")).asString().getStatus());
|
||||
assertEquals(
|
||||
200,
|
||||
Unirest.get(endpoint("/")).asString().getStatus()
|
||||
);
|
||||
assertEquals(
|
||||
200,
|
||||
Unirest.get(endpoint("/index.html")).asString().getStatus()
|
||||
@@ -689,4 +760,56 @@ public class ApiServerTest {
|
||||
Unirest.put(url + "?venue=asdf&algorithm=" + algorithms.get(0)).asString().getStatus()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("auth")
|
||||
@Order(3000)
|
||||
void test_publicEndpoint() throws Exception {
|
||||
// no token
|
||||
HttpResponse<String> resp =
|
||||
Unirest.get(endpoint("/api/v1/getTopology", false)).asString();
|
||||
assertEquals(403, resp.getStatus());
|
||||
|
||||
// bad token
|
||||
resp = Unirest.get(endpoint("/api/v1/getTopology", false))
|
||||
.header("Authorization", "Bearer some_bad_token")
|
||||
.asString();
|
||||
assertEquals(403, resp.getStatus());
|
||||
|
||||
// valid for 300 seconds (5 minutes)
|
||||
String token = "this_is_a_good_token";
|
||||
owSecService.addToken(token, 300);
|
||||
// good token
|
||||
resp = Unirest.get(endpoint("/api/v1/getTopology", false))
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.asString();
|
||||
assertEquals(200, resp.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Tag("auth")
|
||||
@Order(3001)
|
||||
void test_privateEndpoint() throws Exception {
|
||||
// no headers
|
||||
HttpResponse<String> resp =
|
||||
Unirest.get(endpoint("/api/v1/getTopology", true)).asString();
|
||||
assertEquals(403, resp.getStatus());
|
||||
|
||||
// bad headers
|
||||
resp = Unirest.get(endpoint("/api/v1/getTopology", true))
|
||||
.header("X-INTERNAL-NAME", "internal_name")
|
||||
.header("X-API-KEY", "not_a_valid_key")
|
||||
.asString();
|
||||
assertEquals(403, resp.getStatus());
|
||||
|
||||
// good headers
|
||||
resp = Unirest.get(endpoint("/api/v1/getTopology", true))
|
||||
.header("X-INTERNAL-NAME", "internal_name")
|
||||
.header(
|
||||
"X-API-KEY",
|
||||
Utils.generateServiceKey(rrmConfig.serviceConfig)
|
||||
)
|
||||
.asString();
|
||||
assertEquals(200, resp.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.openwifi.rrm.services;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.time.Instant;
|
||||
|
||||
import com.facebook.openwifi.cloudsdk.models.gw.TokenValidationResult;
|
||||
import com.facebook.openwifi.cloudsdk.models.gw.UserInfo;
|
||||
import com.facebook.openwifi.cloudsdk.models.gw.WebTokenResult;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import spark.Service;
|
||||
import spark.Request;
|
||||
import spark.Response;
|
||||
import spark.Route;
|
||||
|
||||
/**
|
||||
* This is a mock OW Security service meant to be used in tests.
|
||||
*
|
||||
* @see <a href="https://github.com/Telecominfraproject/wlan-cloud-ucentralsec">owsec</a>
|
||||
*/
|
||||
public class MockOWSecService {
|
||||
private class TokenInfo {
|
||||
long expiry;
|
||||
long created;
|
||||
}
|
||||
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
/** A mapping of valid tokens to their expiry time in seconds since epoch */
|
||||
private Map<String, TokenInfo> validTokens;
|
||||
|
||||
/** The Spark service */
|
||||
private Service service;
|
||||
|
||||
public MockOWSecService(int port) {
|
||||
validTokens = new HashMap<>();
|
||||
service = Service.ignite();
|
||||
service.port(port);
|
||||
|
||||
service.get("/api/v1/validateToken", new ValidateTokenEndpoint());
|
||||
service.get("/api/v1/oauth2", new ValidateTokenEndpoint());
|
||||
service.get("/api/v1/systemEndpoints", new SystemEndpoint());
|
||||
|
||||
service.awaitInitialization();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
service.stop();
|
||||
service.awaitStop();
|
||||
}
|
||||
|
||||
public void addToken(String token, long expiresInSec) {
|
||||
TokenInfo time = new TokenInfo();
|
||||
time.created = Instant.now().getEpochSecond();
|
||||
time.expiry = expiresInSec;
|
||||
|
||||
validTokens.put(token, time);
|
||||
}
|
||||
|
||||
public void removeToken(String token) {
|
||||
validTokens.remove(token);
|
||||
}
|
||||
|
||||
public int getPort() { return service.port(); }
|
||||
|
||||
public class Oauth2Endpoint implements Route {
|
||||
@Override
|
||||
public String handle(Request request, Response response) {
|
||||
response.status(501);
|
||||
return "Not Implemented";
|
||||
}
|
||||
}
|
||||
|
||||
public class SystemEndpoint implements Route {
|
||||
@Override
|
||||
public String handle(Request request, Response response) {
|
||||
response.status(501);
|
||||
return "Not Implemented";
|
||||
}
|
||||
}
|
||||
|
||||
public class ValidateTokenEndpoint implements Route {
|
||||
@Override
|
||||
public String handle(Request request, Response response) {
|
||||
String token = request.queryParams("token");
|
||||
if (token == null) {
|
||||
response.status(403);
|
||||
return "Forbidden";
|
||||
}
|
||||
|
||||
TokenInfo info = validTokens.get(token);
|
||||
if (info == null) {
|
||||
response.status(403);
|
||||
return "Forbidden";
|
||||
}
|
||||
|
||||
if (info.created + info.expiry < Instant.now().getEpochSecond()) {
|
||||
response.status(403);
|
||||
return "Forbidden";
|
||||
}
|
||||
|
||||
TokenValidationResult result = new TokenValidationResult();
|
||||
result.userInfo = new UserInfo();
|
||||
result.tokenInfo = new WebTokenResult();
|
||||
result.tokenInfo.access_token = token;
|
||||
result.tokenInfo.created = info.created;
|
||||
result.tokenInfo.expires_in = info.expiry;
|
||||
|
||||
return gson.toJson(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@ import http from 'k6/http';
|
||||
import { sleep } from 'k6';
|
||||
|
||||
|
||||
const BASE_URL = 'http://localhost:16789/api/v1';
|
||||
const INTERNAL_BASE_URL = 'http://localhost:16790/api/v1';
|
||||
const EXTERNAL_BASE_URL = __ENV.SEPARATE_INTERNAL_EXTERNAL_PORTS ? 'http://localhost:16789/api/v1' : INTERNAL_BASE_URL;
|
||||
|
||||
|
||||
export default function () {
|
||||
const endpoints = [
|
||||
@@ -12,13 +14,19 @@ export default function () {
|
||||
'getToplogy',
|
||||
'currentModel',
|
||||
];
|
||||
const requests = endpoints.map(endpoint => {
|
||||
const internalRequests = endpoints.map(endpoint => {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: `${BASE_URL}/${endpoint}`,
|
||||
url: `${INTERNAL_BASE_URL}/${endpoint}`,
|
||||
};
|
||||
});
|
||||
const externalRequests = endpoints.map(endpoint => {
|
||||
return {
|
||||
method: 'GET',
|
||||
url: `${EXTERNAL_BASE_URL}/${endpoint}`,
|
||||
};
|
||||
});
|
||||
|
||||
let responses = http.batch(requests);
|
||||
let responses = http.batch([...internalRequests, ...externalRequests]);
|
||||
sleep(0.1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user