-
Ryan Izard authored
Updated Restlet from 2.2M3 to 2.3.1. Included with the update is full SSL support for the REST API, either all-access+encryption or restricted-access+encryption (truststore verified clients only). Additional options have been added to floodlightdefault.properties in order to turn HTTPS on/off. HTTPS ccan be used in conjunction with vanilla HTTP as well -- each must reside on a different TCP port though. Also, this time, I remembered to update and test the build.xml file to remove the old and include the new Restlet jars.
Ryan Izard authoredUpdated Restlet from 2.2M3 to 2.3.1. Included with the update is full SSL support for the REST API, either all-access+encryption or restricted-access+encryption (truststore verified clients only). Additional options have been added to floodlightdefault.properties in order to turn HTTPS on/off. HTTPS ccan be used in conjunction with vanilla HTTP as well -- each must reside on a different TCP port though. Also, this time, I remembered to update and test the build.xml file to remove the old and include the new Restlet jars.
RestApiServer.java 11.21 KiB
/**
* Copyright 2013, Big Switch Networks, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
**/
package net.floodlightcontroller.restserver;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.restlet.Application;
import org.restlet.Component;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.Restlet;
import org.restlet.Server;
import org.restlet.data.Parameter;
import org.restlet.data.Protocol;
import org.restlet.data.Reference;
import org.restlet.data.Status;
import org.restlet.ext.jackson.JacksonRepresentation;
import org.restlet.representation.Representation;
import org.restlet.routing.Filter;
import org.restlet.routing.Router;
import org.restlet.routing.Template;
import org.restlet.service.StatusService;
import org.restlet.util.Series;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.floodlightcontroller.core.internal.FloodlightProvider;
import net.floodlightcontroller.core.module.FloodlightModuleContext;
import net.floodlightcontroller.core.module.FloodlightModuleException;
import net.floodlightcontroller.core.module.IFloodlightModule;
import net.floodlightcontroller.core.module.IFloodlightService;
public class RestApiServer implements IFloodlightModule, IRestApiService {
protected static Logger logger = LoggerFactory.getLogger(RestApiServer.class);
protected List<RestletRoutable> restlets;
protected FloodlightModuleContext fmlContext;
protected String restHost = null;
private static String keyStorePassword;
private static String keyStore;
private static String httpsNeedClientAuth = "true";
private static boolean useHttps = false;
private static boolean useHttp = false;
private static String httpsPort;
private static String httpPort;
// ***********
// Application
// ***********
protected class RestApplication extends Application {
protected Context context;
public RestApplication() {
super(new Context());
this.context = getContext();
}
@Override
public Restlet createInboundRoot() {
Router baseRouter = new Router(context);
baseRouter.setDefaultMatchingMode(Template.MODE_STARTS_WITH);
for (RestletRoutable rr : restlets) {
baseRouter.attach(rr.basePath(), rr.getRestlet(context));
}
Filter slashFilter = new Filter() {
@Override
protected int beforeHandle(Request request, Response response) {
Reference ref = request.getResourceRef();
String originalPath = ref.getPath();
if (originalPath.contains("//"))
{
String newPath = originalPath.replaceAll("/+", "/");
ref.setPath(newPath);
}
return Filter.CONTINUE;
}
};
slashFilter.setNext(baseRouter);
return slashFilter;
}
public void run(FloodlightModuleContext fmlContext, String restHost) {
setStatusService(new StatusService() {
@Override
public Representation getRepresentation(Status status,
Request request,
Response response) {
return new JacksonRepresentation<Status>(status);
}
});
// Add everything in the module context to the rest
for (Class<? extends IFloodlightService> s : fmlContext.getAllServices()) {
if (logger.isTraceEnabled()) {
logger.trace("Adding {} for service {} into context",
s.getCanonicalName(), fmlContext.getServiceImpl(s));
}
context.getAttributes().put(s.getCanonicalName(),
fmlContext.getServiceImpl(s));
}
/*
* Specifically add the FML for use by the REST API's /wm/core/modules/...
*/
context.getAttributes().put(fmlContext.getModuleLoader().getClass().getCanonicalName(), fmlContext.getModuleLoader());
/* Start listening for REST requests */
try {
final Component component = new Component();
if (RestApiServer.useHttps) {
Server server;
if (restHost == null) {
server = component.getServers().add(Protocol.HTTPS, Integer.valueOf(RestApiServer.httpsPort));
} else {
server = component.getServers().add(Protocol.HTTPS, restHost, Integer.valueOf(RestApiServer.httpsPort));
}
Series<Parameter> parameters = server.getContext().getParameters();
//parameters.add("sslContextFactory", "org.restlet.ext.jsslutils.PkixSslContextFactory");
parameters.add("sslContextFactory", "org.restlet.engine.ssl.DefaultSslContextFactory");
parameters.add("keystorePath", RestApiServer.keyStore);
parameters.add("keystorePassword", RestApiServer.keyStorePassword);
parameters.add("keyPassword", RestApiServer.keyStorePassword);
parameters.add("keystoreType", "JKS");
parameters.add("truststorePath", RestApiServer.keyStore);
parameters.add("truststorePassword", RestApiServer.keyStorePassword);
parameters.add("trustPassword", RestApiServer.keyStorePassword);
parameters.add("truststoreType", "JKS");
parameters.add("needClientAuthentication", RestApiServer.httpsNeedClientAuth);
}
if (RestApiServer.useHttp) {
if (restHost == null) {
component.getServers().add(Protocol.HTTP, Integer.valueOf(RestApiServer.httpPort));
} else {
component.getServers().add(Protocol.HTTP, restHost, Integer.valueOf(RestApiServer.httpPort));
}
}
component.getClients().add(Protocol.CLAP);
component.getDefaultHost().attach(this);
component.start();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
// ***************
// IRestApiService
// ***************
@Override
public void addRestletRoutable(RestletRoutable routable) {
restlets.add(routable);
}
@Override
public void run() {
if (logger.isDebugEnabled()) {
StringBuffer sb = new StringBuffer();
sb.append("REST API routables: ");
for (RestletRoutable routable : restlets) {
sb.append(routable.getClass().getSimpleName());
sb.append(" (");
sb.append(routable.basePath());
sb.append("), ");
}
logger.debug(sb.toString());
}
RestApplication restApp = new RestApplication();
restApp.run(fmlContext, restHost);
}
// *****************
// IFloodlightModule
// *****************
@Override
public Collection<Class<? extends IFloodlightService>> getModuleServices() {
Collection<Class<? extends IFloodlightService>> services =
new ArrayList<Class<? extends IFloodlightService>>(1);
services.add(IRestApiService.class);
return services;
}
@Override
public Map<Class<? extends IFloodlightService>, IFloodlightService>
getServiceImpls() {
Map<Class<? extends IFloodlightService>,
IFloodlightService> m =
new HashMap<Class<? extends IFloodlightService>,
IFloodlightService>();
m.put(IRestApiService.class, this);
return m;
}
@Override
public Collection<Class<? extends IFloodlightService>> getModuleDependencies() {
// We don't have any
return null;
}
@Override
public void init(FloodlightModuleContext context)
throws FloodlightModuleException {
// This has to be done here since we don't know what order the
// startUp methods will be called
this.restlets = new ArrayList<RestletRoutable>();
this.fmlContext = context;
// read our config options
Map<String, String> configOptions = context.getConfigParams(this);
restHost = configOptions.get("host");
if (restHost == null) {
Map<String, String> providerConfigOptions = context.getConfigParams(
FloodlightProvider.class);
restHost = providerConfigOptions.get("openflowhost");
}
if (restHost != null) {
logger.debug("REST host set to {}", restHost);
}
String path = configOptions.get("keyStorePath");
String pass = configOptions.get("keyStorePassword");
String useHttps = configOptions.get("useHttps");
String useHttp = configOptions.get("useHttp");
String httpsNeedClientAuth = configOptions.get("httpsNeedClientAuthentication");
/* HTTPS Access (ciphertext) */
if (useHttps == null || path == null || path.isEmpty() ||
(!useHttps.trim().equalsIgnoreCase("yes") && !useHttps.trim().equalsIgnoreCase("true") &&
!useHttps.trim().equalsIgnoreCase("yep") && !useHttps.trim().equalsIgnoreCase("ja") &&
!useHttps.trim().equalsIgnoreCase("stimmt")
)
) {
RestApiServer.useHttps = false;
RestApiServer.keyStore = null;
RestApiServer.keyStorePassword = null;
} else {
RestApiServer.useHttps = true;
RestApiServer.keyStore = path;
RestApiServer.keyStorePassword = (pass == null ? "" : pass);
String port = configOptions.get("httpsPort");
if (port != null && !port.isEmpty()) {
RestApiServer.httpsPort = port.trim();
}
if (httpsNeedClientAuth == null || (!httpsNeedClientAuth.trim().equalsIgnoreCase("yes") &&
!httpsNeedClientAuth.trim().equalsIgnoreCase("true") &&
!httpsNeedClientAuth.trim().equalsIgnoreCase("yep") &&
!httpsNeedClientAuth.trim().equalsIgnoreCase("ja") &&
!httpsNeedClientAuth.trim().equalsIgnoreCase("stimmt"))
) {
RestApiServer.httpsNeedClientAuth = "false";
} else {
RestApiServer.httpsNeedClientAuth = "true";
}
}
/* HTTP Access (plaintext) */
if (useHttp == null || path == null || path.isEmpty() ||
(!useHttp.trim().equalsIgnoreCase("yes") && !useHttp.trim().equalsIgnoreCase("true") &&
!useHttp.trim().equalsIgnoreCase("yep") && !useHttp.trim().equalsIgnoreCase("ja") &&
!useHttp.trim().equalsIgnoreCase("stimmt")
)
) {
RestApiServer.useHttp = false;
} else {
RestApiServer.useHttp = true;
String port = configOptions.get("httpPort");
if (port != null && !port.isEmpty()) {
RestApiServer.httpPort = port.trim();
}
}
if (RestApiServer.useHttp && RestApiServer.useHttps && RestApiServer.httpPort.equals(RestApiServer.httpsPort)) {
logger.error("REST API's HTTP and HTTPS ports cannot be the same. Got " + RestApiServer.httpPort + " for both.");
throw new IllegalArgumentException("REST API's HTTP and HTTPS ports cannot be the same. Got " + RestApiServer.httpPort + " for both.");
}
if (!RestApiServer.useHttps) {
logger.warn("HTTPS disabled; HTTPS will not be used to connect to the REST API.");
} else {
if (RestApiServer.httpsNeedClientAuth.equals("true")) {
logger.warn("HTTPS enabled; Only trusted clients permitted. Allowing secure access to REST API on port {}.", RestApiServer.httpsPort);
} else {
logger.warn("HTTPS enabled; All clients permitted. Allowing secure access to REST API on port {}.", RestApiServer.httpsPort);
}
logger.info("HTTPS' SSL keystore/truststore path: {}, password: {}", RestApiServer.keyStore, RestApiServer.keyStorePassword);
}
if (!RestApiServer.useHttp) {
logger.warn("HTTP disabled; HTTP will not be used to connect to the REST API.");
} else {
logger.warn("HTTP enabled; Allowing unsecure access to REST API on port {}.", RestApiServer.httpPort);
}
}
@Override
public void startUp(FloodlightModuleContext Context) {
// no-op
}
}