/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.nifi.cluster.firewall.impl; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collection; import org.apache.commons.net.util.SubnetUtils; import org.apache.nifi.cluster.firewall.ClusterNodeFirewall; import org.apache.nifi.logging.NiFiLog; import org.apache.nifi.util.file.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A file-based implementation of the ClusterFirewall interface. The class is configured with a file. If the file is empty, then everything is permissible. Otherwise, the file should contain hostnames * or IPs formatted as dotted decimals with an optional CIDR suffix. Each entry must be separated by a newline. An example configuration is given below: * * <code> * # hash character is a comment delimiter * 1.2.3.4 # exact IP * some.host.name # a host name * 4.5.6.7/8 # range of CIDR IPs * 9.10.11.12/13 # a smaller range of CIDR IPs * </code> * * This class allows for synchronization with an optionally configured restore directory. If configured, then at startup, if the either the config file or the restore directory's copy is missing, then * the configuration file will be copied to the appropriate location. If both restore directory contains a copy that is different in content to configuration file, then an exception is thrown at * construction time. */ public class FileBasedClusterNodeFirewall implements ClusterNodeFirewall { private final File config; private final File restoreDirectory; private final Collection<SubnetUtils.SubnetInfo> subnetInfos = new ArrayList<>(); private static final Logger logger = new NiFiLog(LoggerFactory.getLogger(FileBasedClusterNodeFirewall.class)); public FileBasedClusterNodeFirewall(final File config) throws IOException { this(config, null); } public FileBasedClusterNodeFirewall(final File config, final File restoreDirectory) throws IOException { if (config == null) { throw new IllegalArgumentException("Firewall configuration file may not be null."); } this.config = config; this.restoreDirectory = restoreDirectory; if (restoreDirectory != null) { // synchronize with restore directory try { syncWithRestoreDirectory(); } catch (final IOException ioe) { throw new RuntimeException(ioe); } } if (!config.exists() && !config.createNewFile()) { throw new IOException("Firewall configuration file did not exist and could not be created: " + config.getAbsolutePath()); } logger.info("Loading cluster firewall configuration."); parseConfig(config); logger.info("Cluster firewall configuration loaded."); } @Override public boolean isPermissible(final String hostOrIp) { try { // if no rules, then permit everything if (subnetInfos.isEmpty()) { return true; } final String ip; try { ip = InetAddress.getByName(hostOrIp).getHostAddress(); } catch (final UnknownHostException uhe) { logger.warn("Blocking unknown host '{}'", hostOrIp, uhe); return false; } // check each subnet to see if IP is in range for (final SubnetUtils.SubnetInfo subnetInfo : subnetInfos) { if (subnetInfo.isInRange(ip)) { return true; } } // no match logger.debug("Blocking host '{}' because it does not match our allowed list.", hostOrIp); return false; } catch (final IllegalArgumentException iae) { logger.debug("Blocking requested host, '{}', because it is malformed.", hostOrIp, iae); return false; } } private void syncWithRestoreDirectory() throws IOException { // sanity check that restore directory is a directory, creating it if necessary FileUtils.ensureDirectoryExistAndCanAccess(restoreDirectory); // check that restore directory is not the same as the primary directory if (config.getParentFile().getAbsolutePath().equals(restoreDirectory.getAbsolutePath())) { throw new IllegalStateException( String.format("Cluster firewall configuration file '%s' cannot be in the restore directory '%s' ", config.getAbsolutePath(), restoreDirectory.getAbsolutePath())); } // the restore copy will have same file name, but reside in a different directory final File restoreFile = new File(restoreDirectory, config.getName()); // sync the primary copy with the restore copy FileUtils.syncWithRestore(config, restoreFile, logger); } private void parseConfig(final File config) throws IOException { // clear old information subnetInfos.clear(); try (BufferedReader br = new BufferedReader(new FileReader(config))) { String ipOrHostLine; String ipCidr; int totalIpsAdded = 0; while ((ipOrHostLine = br.readLine()) != null) { // cleanup whitespace ipOrHostLine = ipOrHostLine.trim(); if (ipOrHostLine.isEmpty() || ipOrHostLine.startsWith("#")) { // skip empty lines or comments continue; } else if (ipOrHostLine.contains("#")) { // parse out comments in IP containing lines ipOrHostLine = ipOrHostLine.substring(0, ipOrHostLine.indexOf("#")).trim(); } // if given a complete IP, then covert to CIDR if (ipOrHostLine.contains("/")) { ipCidr = ipOrHostLine; } else if (ipOrHostLine.contains("\\")) { logger.warn("CIDR IP notation uses forward slashes '/'. Replacing backslash '\\' with forward slash'/' for '{}'", ipOrHostLine); ipCidr = ipOrHostLine.replace("\\", "/"); } else { try { ipCidr = InetAddress.getByName(ipOrHostLine).getHostAddress(); if (!ipOrHostLine.equals(ipCidr)) { logger.debug("Resolved host '{}' to ip '{}'", ipOrHostLine, ipCidr); } ipCidr += "/32"; logger.debug("Adding CIDR to exact IP: '{}'", ipCidr); } catch (final UnknownHostException uhe) { logger.warn("Firewall is skipping unknown host address: '{}'", ipOrHostLine); continue; } } try { logger.debug("Adding CIDR IP to firewall: '{}'", ipCidr); final SubnetUtils subnetUtils = new SubnetUtils(ipCidr); subnetUtils.setInclusiveHostCount(true); subnetInfos.add(subnetUtils.getInfo()); totalIpsAdded++; } catch (final IllegalArgumentException iae) { logger.warn("Firewall is skipping invalid CIDR address: '{}'", ipOrHostLine); } } if (totalIpsAdded == 0) { logger.info("No IPs added to firewall. Firewall will accept all requests."); } else { logger.info("Added {} IP(s) to firewall. Only requests originating from the configured IPs will be accepted.", totalIpsAdded); } } } }