/* * Copyright (c) 2016 Saugo360 and others. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 which accompanies this distribution, * and is available at http://www.eclipse.org/legal/epl-v10.html */ package org.opendaylight.tsdr.restconf.collector; import java.lang.invoke.MethodHandles; import java.util.Arrays; import java.util.Dictionary; import java.util.Hashtable; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class is responsible for reading the configuration of the module from its configuration file, and providing * this configuration for the other classes. In addition, the class validates the values of the configuration items * and replaces them with default values in case they were invalid. * It is worth mentioning that there were two options for storing the configuration; the first was in a configuration * file, the second was in the MD-SAL. I chose the former, because one of the main reasons for creating this module, * is being able to review restconf accesses in case of penetration, in order to see what the intruder has, read, * changed, deleted, etc. However, if the configuration is stored in the MD-SAL, the intruder could start by switching * off the collector (since MD-SAL configuration is accessible from restconf), and then doing whatever he/she wants to * do. That's why it is safer to place the configuration in a configuration file. * * @author <a href="mailto:a.alhamali93@gmail.com">AbdulRahman AlHamali</a> * * Created: Dec 16th, 2016 * */ public class TSDRRestconfCollectorConfig implements ManagedService { /** * The instance of this class (for singleton pattern). */ private static TSDRRestconfCollectorConfig INSTANCE = null; /** * The cached values of the configurations. */ private Dictionary<String, String> configurations = new Hashtable<>(); /** * The logger of the class. */ private static Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); /** * The default value for which http methods should be logged. GET has been omitted, since there are many GET * requests that are usually sent everytime DLUX is started, which will result in a lot of noisy data. */ private static final String DEFAULT_METHODS_TO_LOG = "POST,PUT,DELETE"; /** * The default value for which paths in the tree should be logged. */ private static final String DEFAULT_PATHS_TO_LOG = ".*"; /** * The default value for which IP addresses to log from. */ private static final String DEFAULT_REMOTE_ADDRESSES_TO_LOG = ".*"; /** * The default value for what content of requests should be logged. */ private static final String DEFAULT_CONTENT_TO_LOG = ".*"; /** * An array of possible http methods. There are other methods but they are not used in RESTCONF. */ private static final String[] HTTP_METHODS = {"POST", "PUT", "GET", "DELETE"}; private TSDRRestconfCollectorConfig() { } /** * returns the instance of the singleton. * @return the instance of the class. If the instance was null, a new instance is created */ public static TSDRRestconfCollectorConfig getInstance() { if (INSTANCE == null) { INSTANCE = new TSDRRestconfCollectorConfig(); } return INSTANCE; } /** * only call this method in testing to do mocking. * @param instance the instance of the class */ public static void setInstance(TSDRRestconfCollectorConfig instance) { INSTANCE = instance; } /** * only call this method in testing to do mocking. * @param logger the instance of the logger */ public static void setLogger(Logger logger) { log = logger; } /** * this function is called automatically everytime the configuration of the collector changes, and when the module * is first started. The function validates the configuration values and caches them for later use. * @param properties the properties of the collector */ @Override public synchronized void updated(Dictionary properties) throws ConfigurationException { if (properties != null && !properties.isEmpty()) { String methods = (String)properties.get("METHODS_TO_LOG"); if (!validateMethods(methods)) { log.error("Value specified for METHODS_TO_LOG: " + methods + " is invalid. Will use default value of " + DEFAULT_METHODS_TO_LOG); methods = DEFAULT_METHODS_TO_LOG; } configurations.put("METHODS_TO_LOG", methods); String paths = (String)properties.get("PATHS_TO_LOG"); if (!validatePaths(paths)) { log.error("Value specified for PATHS_TO_LOG: " + paths + " is invalid. Will use default value of " + DEFAULT_PATHS_TO_LOG); paths = DEFAULT_PATHS_TO_LOG; } configurations.put("PATHS_TO_LOG", paths); String remoteAddresses = (String)properties.get("REMOTE_ADDRESSES_TO_LOG"); if (!validateRemoteAddresses(remoteAddresses)) { log.error("Value specified for REMOTE_ADDRESSES_TO_LOG: " + remoteAddresses + " is invalid. Will use default value of " + DEFAULT_REMOTE_ADDRESSES_TO_LOG); remoteAddresses = DEFAULT_REMOTE_ADDRESSES_TO_LOG; } configurations.put("REMOTE_ADDRESSES_TO_LOG", remoteAddresses); String content = (String)properties.get("CONTENT_TO_LOG"); if (!validateContent(content)) { log.error("Value specified for CONTENT_TO_LOG: " + content + " is invalid. Will use default value of " + DEFAULT_CONTENT_TO_LOG); content = DEFAULT_CONTENT_TO_LOG; } configurations.put("CONTENT_TO_LOG", content); } else { log.error("The configuration properties are either empty or non-existent will use default values of: " + "METHODS_TO_LOG=" + DEFAULT_METHODS_TO_LOG + " PATHS_TO_LOG=" + DEFAULT_PATHS_TO_LOG + " REMOTE_ADDRESSES_TO_LOG=" + DEFAULT_REMOTE_ADDRESSES_TO_LOG + " CONTENT_TO_LOG=" + DEFAULT_CONTENT_TO_LOG); configurations.put("METHODS_TO_LOG", DEFAULT_METHODS_TO_LOG); configurations.put("PATHS_TO_LOG", DEFAULT_PATHS_TO_LOG); configurations.put("REMOTE_ADDRESSES_TO_LOG", DEFAULT_REMOTE_ADDRESSES_TO_LOG); configurations.put("CONTENT_TO_LOG", DEFAULT_CONTENT_TO_LOG); } } /** * called by users of the class to retrieve properties of the collector. * @param name the name of the property whose value we want to retrieve * @return the value of the property */ public String getProperty(String name) { return (String)this.configurations.get(name); } /** * validates that the list of http methods is valid to be used. * the list should be comma-separated, should contain valid HTTP methods, and should not have the methods repeated * multiple times * @param methds the list of http methods to validate * @return returns true if the list is valid */ private boolean validateMethods(String methods) { if (methods != null && !methods.equals("")) { // We check the format of the methods String[] methodsArray = methods.split(","); // Each element in the array needs to be one of the known HTTP methods, // and needs to be unique for (int i = 0; i < methodsArray.length; i++) { if (!Arrays.asList(HTTP_METHODS).contains(methodsArray[i])) { log.error("HTTP method " + methodsArray[i] + " is not recognized"); return false; } for (int j = 0; j < methodsArray.length; j++) { if (i == j) { continue; } if (methodsArray[i].equals(methodsArray[j])) { log.error("HTTP method " + methodsArray[i] + " is repeated multiple times"); return false; } } } return true; } log.error("HTTP_METHODS_TO_LOG is either empty or non-existent"); return false; } /** * validates that the path specified is valid to be used. * the path should be a valid regular expression * @param paths the regular expression of the paths * @return returns true if the paths expression is a valid regular expression */ private boolean validatePaths(String paths) { if (paths != null && !paths.equals("")) { try { Pattern.compile(paths); } catch (PatternSyntaxException exception) { log.error("Pattern " + paths + " is not parsable. Error: " + exception.toString()); return false; } return true; } log.error("PATHS_TO_LOG is either empty or non-existent"); return false; } /** * validates that the remote addresses specified is valid to be used. * the remote addresses expression should be a valid regular expression * @param addresses the regular expression of the remote addresses * @return returns true if the remote addresses expression is a valid regular expression */ private boolean validateRemoteAddresses(String addresses) { if (addresses != null && !addresses.equals("")) { try { Pattern.compile(addresses); } catch (PatternSyntaxException exception) { log.error("Pattern " + addresses + " is not parsable. Error: " + exception.toString()); return false; } return true; } log.error("REMOTE_ADDRESSES_TO_LOG is either empty or non-existent"); return false; } /** * validates that the content specified is valid to be used. * the content should be a valid regular expression * @param content the regular expression of the paths * @return returns true if the content expression is a valid regular expression */ private boolean validateContent(String content) { if (content != null && !content.equals("")) { try { Pattern.compile(content); } catch (PatternSyntaxException exception) { log.error("Pattern " + content + " is not parsable. Error: " + exception.toString()); return false; } return true; } log.error("CONTENT_TO_LOG is either empty or non-existent"); return false; } }