/*
* 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 org.ngrinder.sm;
import java.io.File;
import java.io.FileDescriptor;
import java.net.InetAddress;
import java.security.Permission;
import java.util.ArrayList;
import java.util.List;
/**
* nGrinder security manager.
*
* @author JunHo Yoon
* @author Tobi
* @since 3.0
*/
public class NGrinderSecurityManager extends SecurityManager {
private String workDirectory = System.getProperty("user.dir");
private String agentExecDirectory = System.getProperty("ngrinder.exec.path", workDirectory);
private String javaHomeDirectory = System.getenv("JAVA_HOME");
private String jreHomeDirectory = System.getProperty("java.home");
private final String javaExtDirectory = System.getProperty("java.ext.dirs");
private final String pythonPath = System.getProperty("python.path");
private final String pythonHome = System.getProperty("python.home");
private final String pythonCache = System.getProperty("python.cachedir");
private final String etcHosts = System.getProperty("ngrinder.etc.hosts", "");
private final String consoleIP = System.getProperty("ngrinder.console.ip", "127.0.0.1");
private final List<String> allowedHost = new ArrayList<String>();
private final List<String> readAllowedDirectory = new ArrayList<String>();
private final List<String> writeAllowedDirectory = new ArrayList<String>();
private final List<String> deleteAllowedDirectory = new ArrayList<String>();
{
this.init();
}
void init() {
this.initAccessOfDirectories();
this.initAccessOfHosts();
}
/**
* Set default accessed of directories. <br>
*/
private void initAccessOfDirectories() {
workDirectory = normalize(new File(workDirectory).getAbsolutePath(), null);
String logDirectory;
if (workDirectory != null && !workDirectory.isEmpty()) {
logDirectory = workDirectory.substring(0, workDirectory.lastIndexOf(File.separator));
logDirectory = logDirectory.substring(0, workDirectory.lastIndexOf(File.separator)) + File.separator
+ "log";
} else {
logDirectory = "log";
}
agentExecDirectory = normalize(new File(agentExecDirectory).getAbsolutePath(), null);
if (javaHomeDirectory == null) {
System.out.println("env var JAVA_HOME should be provided.");
} else {
javaHomeDirectory = normalize(new File(javaHomeDirectory).getAbsolutePath(), null);
readAllowedDirectory.add(javaHomeDirectory);
}
readAllowedDirectory.add(workDirectory);
readAllowedDirectory.add(logDirectory);
readAllowedDirectory.add(agentExecDirectory);
readAllowedDirectory.add(jreHomeDirectory);
if (isNotEmpty(pythonHome)) {
readAllowedDirectory.add(pythonHome);
}
if (isNotEmpty(pythonPath)) {
readAllowedDirectory.add(pythonPath);
}
if (isNotEmpty(pythonCache)) {
writeAllowedDirectory.add(pythonCache);
}
readAllowedDirectory.add(getTempDirectoryPath());
readAllowedDirectory.add("/dev/");
String[] jed = javaExtDirectory.split(";");
for (String je : jed) {
je = normalize(new File(je).getAbsolutePath(), null);
readAllowedDirectory.add(je);
}
if (isNotEmpty(pythonHome)) {
writeAllowedDirectory.add(pythonHome);
}
if (isNotEmpty(pythonPath)) {
writeAllowedDirectory.add(pythonPath);
}
if (isNotEmpty(pythonCache)) {
writeAllowedDirectory.add(pythonCache);
}
writeAllowedDirectory.add(workDirectory);
writeAllowedDirectory.add(logDirectory);
writeAllowedDirectory.add(getTempDirectoryPath());
deleteAllowedDirectory.add(workDirectory);
}
private static boolean isNotEmpty(String str) {
return str != null && str.length() != 0;
}
// -----------------------------------------------------------------------
/**
* Returns the path to the system temporary directory.
*
* @return the path to the system temporary directory.
* @since Commons IO 2.0
*/
private static String getTempDirectoryPath() {
return System.getProperty("java.io.tmpdir");
}
/**
* Get ip address of target hosts. <br>
* if target hosts 'a.com:1.1.1.1' add 'a.com' & '1.1.1.1' <br>
* if target hosts ':1.1.1.1' add : '1.1.1.1' <br>
* if target hosts '1.1.1.1' add : '1.1.1.1' <br>
* <br>
* Add controller host<br>
*/
private void initAccessOfHosts() {
String[] hostsList = etcHosts.split(",");
for (String hosts : hostsList) {
String[] addresses = hosts.split(":");
if (addresses.length > 1) {
allowedHost.add(addresses[0]);
allowedHost.add(addresses[addresses.length - 1]);
} else {
allowedHost.add(hosts);
}
}
// add controller host
allowedHost.add(consoleIP);
try {
java.security.Security.setProperty("networkaddress.cache.ttl", "0");
} catch (Exception e) {
// Fall through
}
}
@Override
public void checkPermission(Permission permission) {
if (permission instanceof java.lang.RuntimePermission) {
// except setSecurityManager
String permissionName = permission.getName();
if ("setSecurityManager".equals(permissionName)) {
processSetSecurityManagerAction();
}
} else if (permission instanceof java.security.UnresolvedPermission) {
throw new SecurityException("java.security.UnresolvedPermission is not allowed.");
} else if (permission instanceof java.awt.AWTPermission) {
throw new SecurityException("java.awt.AWTPermission is not allowed.");
} else if (permission instanceof javax.security.auth.AuthPermission) {
throw new SecurityException("javax.security.auth.AuthPermission is not allowed.");
} else if (permission instanceof javax.security.auth.PrivateCredentialPermission) {
throw new SecurityException("javax.security.auth.PrivateCredentialPermission is not allowed.");
} else if (permission instanceof javax.security.auth.kerberos.DelegationPermission) {
throw new SecurityException("javax.security.auth.kerberos.DelegationPermission is not allowed.");
} else if (permission instanceof javax.security.auth.kerberos.ServicePermission) {
throw new SecurityException("javax.security.auth.kerberos.ServicePermission is not allowed.");
} else if (permission instanceof javax.sound.sampled.AudioPermission) {
throw new SecurityException("javax.sound.sampled.AudioPermission is not allowed.");
}
}
protected void processSetSecurityManagerAction() throws SecurityException {
throw new SecurityException("java.lang.RuntimePermission: setSecurityManager is not allowed.");
}
@Override
public void checkPermission(Permission permission, Object context) {
this.checkPermission(permission);
}
@Override
public void checkRead(String file) {
if (file != null && file.contains("database.conf")) {
throw new SecurityException("File Read access on database.conf is not allowed.");
}
// fileAccessReadAllowed(file);
}
@Override
public void checkRead(String file, Object context) {
if (file != null && file.contains("database.conf")) {
throw new SecurityException("File Read access on database.conf is not allowed.");
}
}
@Override
public void checkRead(FileDescriptor fd) {
}
@Override
public void checkWrite(String file) {
this.fileAccessWriteAllowed(file);
}
@Override
public void checkDelete(String file) {
this.fileAccessDeleteAllowed(file);
}
@Override
public void checkExec(String cmd) {
throw new SecurityException("Cmd execution of " + cmd + " is not allowed.");
}
/**
* File read access is allowed on <br>
* "agent.exec.folder" and "agent.exec.folder".
*
* @param file file path
*/
@SuppressWarnings("unused")
private void fileAccessReadAllowed(String file) {
// We don't use this for a while.
String filePath = normalize(file, workDirectory);
for (String dir : readAllowedDirectory) {
if (filePath != null && filePath.startsWith(dir)) {
return;
}
}
throw new SecurityException("File Read access on " + file + "(" + filePath + ") is not allowed.");
}
/**
* File write access is allowed <br>
* on "agent.exec.folder".
*
* @param file file path
*/
private void fileAccessWriteAllowed(String file) {
if (file != null && (file.contains("log/test_") || file.contains("log\\test_"))) {
return;
}
String filePath = normalize(file, workDirectory);
for (String dir : writeAllowedDirectory) {
if (filePath != null && filePath.startsWith(dir)) {
return;
}
}
throw new SecurityException("File write access on " + file + "(" + filePath + ") is not allowed.");
}
/**
* File delete access is allowed <br>
* on "agent.exec.folder".
*
* @param file file path
*/
private void fileAccessDeleteAllowed(String file) {
String filePath = normalize(file, workDirectory);
for (String dir : deleteAllowedDirectory) {
if (filePath != null && filePath.startsWith(dir)) {
return;
}
}
throw new SecurityException("File delete access on " + file + "(" + filePath + ") is not allowed.");
}
@Override
public void checkMulticast(InetAddress maddr) {
throw new SecurityException("Multicast on " + maddr.toString() + " is not always allowed.");
}
@Override
public void checkConnect(String host, int port) {
this.netWorkAccessAllowed(host);
}
@Override
public void checkConnect(String host, int port, Object context) {
this.netWorkAccessAllowed(host);
}
private String normalize(String filename, String workingDirectory) {
if (getPrefixLength(filename) == 0 && workingDirectory != null) {
filename = workingDirectory + File.separator + filename;
}
return doNormalize(filename);
}
/**
* NetWork access is allowed on "ngrinder.etc.hosts".
*
* @param host host name
*/
private void netWorkAccessAllowed(String host) {
if (allowedHost.contains(host)) {
return;
}
throw new SecurityException("NetWork access on " + host + " is not allowed. Please add " + host
+ " on the target host setting.");
}
/**
* The system separator character.
*/
private static final char SYSTEM_SEPARATOR = File.separatorChar;
/**
* The Unix separator character.
*/
private static final char UNIX_SEPARATOR = '/';
/**
* The Windows separator character.
*/
private static final char WINDOWS_SEPARATOR = '\\';
/**
* The separator character that is the opposite of the system separator.
*/
private static final char OTHER_SEPARATOR;
static {
if (isSystemWindows()) {
OTHER_SEPARATOR = UNIX_SEPARATOR;
} else {
OTHER_SEPARATOR = WINDOWS_SEPARATOR;
}
}
// -----------------------------------------------------------------------
/**
* Determines if Windows file system is in use.
*
* @return true if the system is Windows
*/
private static boolean isSystemWindows() {
return SYSTEM_SEPARATOR == WINDOWS_SEPARATOR;
}
/**
* Internal method to perform the normalization.
*
* @param filename the filename
* @return the normalized filename
*/
private static String doNormalize(String filename) {
if (filename == null) {
return null;
}
int size = filename.length();
if (size == 0) {
return filename;
}
int prefix = getPrefixLength(filename);
if (prefix < 0) {
return null;
}
char[] array = new char[size + 2]; // +1 for possible extra slash, +2 for arraycopy
filename.getChars(0, filename.length(), array, 0);
for (int i = 0; i < array.length; i++) {
if (array[i] == OTHER_SEPARATOR) {
array[i] = SYSTEM_SEPARATOR;
}
}
// add extra separator on the end to simplify code below
boolean lastIsDirectory = true;
if (array[size - 1] != SYSTEM_SEPARATOR) {
array[size++] = SYSTEM_SEPARATOR;
lastIsDirectory = false;
}
// adjoining slashes
for (int i = prefix + 1; i < size; i++) {
if (array[i] == SYSTEM_SEPARATOR && array[i - 1] == SYSTEM_SEPARATOR) {
System.arraycopy(array, i, array, i - 1, size - i);
size--;
i--;
}
}
// dot slash
for (int i = prefix + 1; i < size; i++) {
if (array[i] == SYSTEM_SEPARATOR && array[i - 1] == '.' && (i == prefix + 1 || array[i - 2] == SYSTEM_SEPARATOR)) {
if (i == size - 1) {
lastIsDirectory = true;
}
System.arraycopy(array, i + 1, array, i - 1, size - i);
size -= 2;
i--;
}
}
// double dot slash
outer:
for (int i = prefix + 2; i < size; i++) {
if (array[i] == SYSTEM_SEPARATOR && array[i - 1] == '.' && array[i - 2] == '.'
&& (i == prefix + 2 || array[i - 3] == SYSTEM_SEPARATOR)) {
if (i == prefix + 2) {
return null;
}
if (i == size - 1) {
lastIsDirectory = true;
}
int j;
for (j = i - 4; j >= prefix; j--) {
if (array[j] == SYSTEM_SEPARATOR) {
// remove b/../ from a/b/../c
System.arraycopy(array, i + 1, array, j + 1, size - i);
size -= (i - j);
i = j + 1;
continue outer;
}
}
// remove a/../ from a/../c
System.arraycopy(array, i + 1, array, prefix, size - i);
size -= (i + 1 - prefix);
i = prefix + 1;
}
}
if (size <= 0) { // should never be less than 0
return "";
}
if (size <= prefix) { // should never be less than prefix
return new String(array, 0, size);
}
if (lastIsDirectory) {
return new String(array, 0, size); // keep trailing separator
}
return new String(array, 0, size - 1); // lose trailing separator
}
// -----------------------------------------------------------------------
/**
* Returns the length of the filename prefix, such as <code>C:/</code> or <code>~/</code>.
* <p/>
* This method will handle a file in either Unix or Windows format.
* <p/>
* The prefix length includes the first slash in the full filename if applicable. Thus, it is possible that the
* length returned is greater than the length of the input string.
* <p/>
* <pre>
* Windows:
* a\b\c.txt --> "" --> relative
* \a\b\c.txt --> "\" --> current drive absolute
* C:a\b\c.txt --> "C:" --> drive relative
* C:\a\b\c.txt --> "C:\" --> absolute
* \\server\a\b\c.txt --> "\\server\" --> UNC
*
* Unix:
* a/b/c.txt --> "" --> relative
* /a/b/c.txt --> "/" --> absolute
* ~/a/b/c.txt --> "~/" --> current user
* ~ --> "~/" --> current user (slash added)
* ~user/a/b/c.txt --> "~user/" --> named user
* ~user --> "~user/" --> named user (slash added)
* </pre>
* <p/>
* The output will be the same irrespective of the machine that the code is running on. ie. both Unix and Windows
* prefixes are matched regardless.
*
* @param filename the filename to find the prefix in, null returns -1
* @return the length of the prefix, -1 if invalid or null
*/
private static int getPrefixLength(String filename) {
if (filename == null) {
return -1;
}
int len = filename.length();
if (len == 0) {
return 0;
}
char ch0 = filename.charAt(0);
if (ch0 == ':') {
return -1;
}
if (len == 1) {
if (ch0 == '~') {
return 2; // return a length greater than the input
}
return (isSeparator(ch0) ? 1 : 0);
} else {
if (ch0 == '~') {
int posUnix = filename.indexOf(UNIX_SEPARATOR, 1);
int posWin = filename.indexOf(WINDOWS_SEPARATOR, 1);
if (posUnix == -1 && posWin == -1) {
return len + 1; // return a length greater than the input
}
posUnix = (posUnix == -1 ? posWin : posUnix);
posWin = (posWin == -1 ? posUnix : posWin);
return Math.min(posUnix, posWin) + 1;
}
char ch1 = filename.charAt(1);
if (ch1 == ':') {
ch0 = Character.toUpperCase(ch0);
if (ch0 >= 'A' && ch0 <= 'Z') {
if (len == 2 || !isSeparator(filename.charAt(2))) {
return 2;
}
return 3;
}
return -1;
} else if (isSeparator(ch0) && isSeparator(ch1)) {
int posUnix = filename.indexOf(UNIX_SEPARATOR, 2);
int posWin = filename.indexOf(WINDOWS_SEPARATOR, 2);
if ((posUnix == -1 && posWin == -1) || posUnix == 2 || posWin == 2) {
return -1;
}
posUnix = (posUnix == -1 ? posWin : posUnix);
posWin = (posWin == -1 ? posUnix : posWin);
return Math.min(posUnix, posWin) + 1;
} else {
return (isSeparator(ch0) ? 1 : 0);
}
}
}
// -----------------------------------------------------------------------
/**
* Checks if the character is a separator.
*
* @param ch the character to check
* @return true if it is a separator character
*/
private static boolean isSeparator(char ch) {
return (ch == UNIX_SEPARATOR) || (ch == WINDOWS_SEPARATOR);
}
}