Java tutorial
/** * 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 * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * 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 com.buaa.cfs.nfs3; import com.buaa.cfs.conf.Configuration; import com.buaa.cfs.constant.CommonConfigurationKeys; import com.buaa.cfs.constant.Nfs3Constant; import com.buaa.cfs.utils.LightWeightCache; import com.buaa.cfs.utils.LightWeightGSet; import com.buaa.cfs.utils.StringUtils; import com.google.common.base.Preconditions; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.commons.net.util.SubnetUtils; import org.apache.commons.net.util.SubnetUtils.SubnetInfo; import java.net.InetAddress; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; /** * This class provides functionality for loading and checking the mapping between client hosts and their access * privileges. */ public class NfsExports { private static NfsExports exports = null; public static synchronized NfsExports getInstance(Configuration conf) { if (exports == null) { String matchHosts = conf.get(CommonConfigurationKeys.NFS_EXPORTS_ALLOWED_HOSTS_KEY, CommonConfigurationKeys.NFS_EXPORTS_ALLOWED_HOSTS_KEY_DEFAULT); int cacheSize = conf.getInt(Nfs3Constant.NFS_EXPORTS_CACHE_SIZE_KEY, Nfs3Constant.NFS_EXPORTS_CACHE_SIZE_DEFAULT); long expirationPeriodNano = conf.getLong(Nfs3Constant.NFS_EXPORTS_CACHE_EXPIRYTIME_MILLIS_KEY, Nfs3Constant.NFS_EXPORTS_CACHE_EXPIRYTIME_MILLIS_DEFAULT) * 1000 * 1000; try { exports = new NfsExports(cacheSize, expirationPeriodNano, matchHosts); } catch (IllegalArgumentException e) { LOG.error("Invalid NFS Exports provided: ", e); return exports; } } return exports; } public static final Log LOG = LogFactory.getLog(NfsExports.class); // only support IPv4 now private static final String IP_ADDRESS = "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})"; private static final String SLASH_FORMAT_SHORT = IP_ADDRESS + "/(\\d{1,3})"; private static final String SLASH_FORMAT_LONG = IP_ADDRESS + "/" + IP_ADDRESS; private static final Pattern CIDR_FORMAT_SHORT = Pattern.compile(SLASH_FORMAT_SHORT); private static final Pattern CIDR_FORMAT_LONG = Pattern.compile(SLASH_FORMAT_LONG); // Hostnames are composed of series of 'labels' concatenated with dots. // Labels can be between 1-63 characters long, and can only take // letters, digits & hyphens. They cannot start and end with hyphens. For // more details, refer RFC-1123 & http://en.wikipedia.org/wiki/Hostname private static final String LABEL_FORMAT = "[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?"; private static final Pattern HOSTNAME_FORMAT = Pattern .compile("^(" + LABEL_FORMAT + "\\.)*" + LABEL_FORMAT + "$"); static class AccessCacheEntry implements LightWeightCache.Entry { private final String hostAddr; private AccessPrivilege access; private final long expirationTime; private LightWeightGSet.LinkedElement next; AccessCacheEntry(String hostAddr, AccessPrivilege access, long expirationTime) { Preconditions.checkArgument(hostAddr != null); this.hostAddr = hostAddr; this.access = access; this.expirationTime = expirationTime; } @Override public int hashCode() { return hostAddr.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof AccessCacheEntry) { AccessCacheEntry entry = (AccessCacheEntry) obj; return this.hostAddr.equals(entry.hostAddr); } return false; } @Override public void setNext(LightWeightGSet.LinkedElement next) { this.next = next; } @Override public LightWeightGSet.LinkedElement getNext() { return this.next; } @Override public void setExpirationTime(long timeNano) { // we set expiration time in the constructor, and the expiration time // does not change } @Override public long getExpirationTime() { return this.expirationTime; } } private final List<Match> mMatches; private final LightWeightCache<AccessCacheEntry, AccessCacheEntry> accessCache; private final long cacheExpirationPeriod; /** * Constructor. * * @param cacheSize The size of the access privilege cache. * @param expirationPeriodNano The period * @param matchHosts string specifying one or multiple matchers. */ NfsExports(int cacheSize, long expirationPeriodNano, String matchHosts) { this.cacheExpirationPeriod = expirationPeriodNano; accessCache = new LightWeightCache<AccessCacheEntry, AccessCacheEntry>(cacheSize, cacheSize, expirationPeriodNano, 0); String[] matchStrings = matchHosts.split(CommonConfigurationKeys.NFS_EXPORTS_ALLOWED_HOSTS_SEPARATOR); mMatches = new ArrayList<Match>(matchStrings.length); for (String mStr : matchStrings) { if (LOG.isDebugEnabled()) { LOG.debug("Processing match string '" + mStr + "'"); } mStr = mStr.trim(); if (!mStr.isEmpty()) { mMatches.add(getMatch(mStr)); } } } /** * Return the configured group list */ public String[] getHostGroupList() { int listSize = mMatches.size(); String[] hostGroups = new String[listSize]; for (int i = 0; i < mMatches.size(); i++) { hostGroups[i] = mMatches.get(i).getHostGroup(); } return hostGroups; } public AccessPrivilege getAccessPrivilege(InetAddress addr) { return getAccessPrivilege(addr.getHostAddress(), addr.getCanonicalHostName()); } AccessPrivilege getAccessPrivilege(String address, String hostname) { long now = System.nanoTime(); AccessCacheEntry newEntry = new AccessCacheEntry(address, AccessPrivilege.NONE, now + this.cacheExpirationPeriod); // check if there is a cache entry for the given address AccessCacheEntry cachedEntry = accessCache.get(newEntry); if (cachedEntry != null && now < cachedEntry.expirationTime) { // get a non-expired cache entry, use it return cachedEntry.access; } else { for (Match match : mMatches) { if (match.isIncluded(address, hostname)) { if (match.accessPrivilege == AccessPrivilege.READ_ONLY) { newEntry.access = AccessPrivilege.READ_ONLY; break; } else if (match.accessPrivilege == AccessPrivilege.READ_WRITE) { newEntry.access = AccessPrivilege.READ_WRITE; } } } accessCache.put(newEntry); return newEntry.access; } } private static abstract class Match { private final AccessPrivilege accessPrivilege; private Match(AccessPrivilege accessPrivilege) { this.accessPrivilege = accessPrivilege; } public abstract boolean isIncluded(String address, String hostname); public abstract String getHostGroup(); } /** * Matcher covering all client hosts (specified by "*") */ private static class AnonymousMatch extends Match { private AnonymousMatch(AccessPrivilege accessPrivilege) { super(accessPrivilege); } @Override public boolean isIncluded(String address, String hostname) { return true; } @Override public String getHostGroup() { return "*"; } } /** * Matcher using CIDR for client host matching */ private static class CIDRMatch extends Match { private final SubnetInfo subnetInfo; private CIDRMatch(AccessPrivilege accessPrivilege, SubnetInfo subnetInfo) { super(accessPrivilege); this.subnetInfo = subnetInfo; } @Override public boolean isIncluded(String address, String hostname) { if (subnetInfo.isInRange(address)) { if (LOG.isDebugEnabled()) { LOG.debug("CIDRNMatcher low = " + subnetInfo.getLowAddress() + ", high = " + subnetInfo.getHighAddress() + ", allowing client '" + address + "', '" + hostname + "'"); } return true; } if (LOG.isDebugEnabled()) { LOG.debug("CIDRNMatcher low = " + subnetInfo.getLowAddress() + ", high = " + subnetInfo.getHighAddress() + ", denying client '" + address + "', '" + hostname + "'"); } return false; } @Override public String getHostGroup() { return subnetInfo.getAddress() + "/" + subnetInfo.getNetmask(); } } /** * Matcher requiring exact string match for client host */ private static class ExactMatch extends Match { private final String ipOrHost; private ExactMatch(AccessPrivilege accessPrivilege, String ipOrHost) { super(accessPrivilege); this.ipOrHost = ipOrHost; } @Override public boolean isIncluded(String address, String hostname) { if (ipOrHost.equalsIgnoreCase(address) || ipOrHost.equalsIgnoreCase(hostname)) { if (LOG.isDebugEnabled()) { LOG.debug("ExactMatcher '" + ipOrHost + "', allowing client " + "'" + address + "', '" + hostname + "'"); } return true; } if (LOG.isDebugEnabled()) { LOG.debug("ExactMatcher '" + ipOrHost + "', denying client " + "'" + address + "', '" + hostname + "'"); } return false; } @Override public String getHostGroup() { return ipOrHost; } } /** * Matcher where client hosts are specified by regular expression */ private static class RegexMatch extends Match { private final Pattern pattern; private RegexMatch(AccessPrivilege accessPrivilege, String wildcard) { super(accessPrivilege); this.pattern = Pattern.compile(wildcard, Pattern.CASE_INSENSITIVE); } @Override public boolean isIncluded(String address, String hostname) { if (pattern.matcher(address).matches() || pattern.matcher(hostname).matches()) { if (LOG.isDebugEnabled()) { LOG.debug("RegexMatcher '" + pattern.pattern() + "', allowing client '" + address + "', '" + hostname + "'"); } return true; } if (LOG.isDebugEnabled()) { LOG.debug("RegexMatcher '" + pattern.pattern() + "', denying client '" + address + "', '" + hostname + "'"); } return false; } @Override public String getHostGroup() { return pattern.toString(); } } /** * Loading a matcher from a string. The default access privilege is read-only. The string contains 1 or 2 parts, * separated by whitespace characters, where the first part specifies the client hosts, and the second part (if * existent) specifies the access privilege of the client hosts. I.e., * <p> * "client-hosts [access-privilege]" */ private static Match getMatch(String line) { String[] parts = line.split("\\s+"); final String host; AccessPrivilege privilege = AccessPrivilege.READ_ONLY; switch (parts.length) { case 1: host = StringUtils.toLowerCase(parts[0]).trim(); break; case 2: host = StringUtils.toLowerCase(parts[0]).trim(); String option = parts[1].trim(); if ("rw".equalsIgnoreCase(option)) { privilege = AccessPrivilege.READ_WRITE; } break; default: throw new IllegalArgumentException("Incorrectly formatted line '" + line + "'"); } if (host.equals("*")) { if (LOG.isDebugEnabled()) { LOG.debug("Using match all for '" + host + "' and " + privilege); } return new AnonymousMatch(privilege); } else if (CIDR_FORMAT_SHORT.matcher(host).matches()) { if (LOG.isDebugEnabled()) { LOG.debug("Using CIDR match for '" + host + "' and " + privilege); } return new CIDRMatch(privilege, new SubnetUtils(host).getInfo()); } else if (CIDR_FORMAT_LONG.matcher(host).matches()) { if (LOG.isDebugEnabled()) { LOG.debug("Using CIDR match for '" + host + "' and " + privilege); } String[] pair = host.split("/"); return new CIDRMatch(privilege, new SubnetUtils(pair[0], pair[1]).getInfo()); } else if (host.contains("*") || host.contains("?") || host.contains("[") || host.contains("]") || host.contains("(") || host.contains(")")) { if (LOG.isDebugEnabled()) { LOG.debug("Using Regex match for '" + host + "' and " + privilege); } return new RegexMatch(privilege, host); } else if (HOSTNAME_FORMAT.matcher(host).matches()) { if (LOG.isDebugEnabled()) { LOG.debug("Using exact match for '" + host + "' and " + privilege); } return new ExactMatch(privilege, host); } else { throw new IllegalArgumentException("Invalid hostname provided '" + host + "'"); } } }