Skip to content
Snippets Groups Projects
  • Ryan Izard's avatar
    2dc31089
    Updated Restlet from 2.2M3 to 2.3.1. Included with the update is full SSL... · 2dc31089
    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.
    2dc31089
    History
    Updated Restlet from 2.2M3 to 2.3.1. Included with the update is full SSL...
    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.
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
	}
}