/* $Id: ManifoldCF.java 988245 2010-08-23 18:39:35Z kwright $ */ /** * 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.manifoldcf.core.system; import org.apache.manifoldcf.core.interfaces.*; import java.lang.reflect.*; import java.io.*; import java.util.*; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.GeneralSecurityException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.InvalidParameterException; import java.security.Key; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; import java.security.spec.KeySpec; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; import javax.crypto.CipherOutputStream; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; public class ManifoldCF { public static final String _rcsid = "@(#)$Id: ManifoldCF.java 988245 2010-08-23 18:39:35Z kwright $"; // Configuration XML node names and attribute names public static final String NODE_LIBDIR = "libdir"; public static final String ATTRIBUTE_PATH = "path"; // This is the unique process identifier, which has to be unique and repeatable within a cluster /** Process ID (no more than 16 characters) */ protected static String processID = null; // "Working directory" /** This is the working directory file object. */ protected static File workingDirectory = null; // Class loader /** The object that manages ManifoldCF plugin class loading. This is initialized when the initialize method is called. */ protected static ManifoldCFResourceLoader resourceLoader = null; // Shutdown hooks /** Temporary file collector */ protected static FileTrack tracker = null; /** Database handle cleanup */ protected static DatabaseShutdown dbShutdown = null; /** Array of cleanup hooks (for managing shutdown) */ protected final static List<IShutdownHook> cleanupHooks = new ArrayList<IShutdownHook>(); /** Array of polling hooks (for managing polling) */ protected final static List<IPollingHook> pollingHooks = new ArrayList<IPollingHook>(); /** Shutdown thread */ protected static Thread shutdownThread; /** Static initializer for setting up shutdown thread */ static { shutdownThread = new ShutdownThread(); try { Runtime.getRuntime().addShutdownHook(shutdownThread); } catch (Exception e) { // Guess we can't do it - dump a trace and continue e.printStackTrace(); } catch (Error e) { e.printStackTrace(); } } // Flag indicating whether system initialized or not, and synchronizer to protect that flag. protected static int initializeLevel = 0; protected static boolean alreadyClosed = false; protected static boolean alreadyShutdown = false; protected static Integer initializeFlagLock = new Integer(0); // Local member variables protected static String mcfVersion = null; protected static String masterDatabaseName = null; protected static String masterDatabaseUsername = null; protected static String masterDatabasePassword = null; protected static ManifoldCFConfiguration localConfiguration = null; protected static long propertyFilelastMod = -1L; protected static String propertyFilePath = null; protected static final String applicationName = "lcf"; // System property names public static final String lcfConfigFileProperty = "org.apache.manifoldcf.configfile"; // System property/config file property names // Version property /** The current ManifoldCF version string */ public static final String versionProperty = "org.apache.manifoldcf.versionstring"; // Process ID property /** Process ID - cannot exceed 16 characters */ public static final String processIDProperty = "org.apache.manifoldcf.processid"; // Database access properties /** Database name property */ public static final String masterDatabaseNameProperty = "org.apache.manifoldcf.database.name"; /** Database user name property */ public static final String masterDatabaseUsernameProperty = "org.apache.manifoldcf.database.username"; /** Database password property */ public static final String masterDatabasePasswordProperty = "org.apache.manifoldcf.database.password"; // Database connection pooling properties /** Maximum open database handles property */ public static final String databaseHandleMaxcountProperty = "org.apache.manifoldcf.database.maxhandles"; /** Database handle timeout property */ public static final String databaseHandleTimeoutProperty = "org.apache.manifoldcf.database.handletimeout"; /** Connection tracking debug property */ public static final String databaseConnectionTrackingProperty = "org.apache.manifoldcf.database.connectiontracking"; // Database performance monitoring properties /** Elapsed time a query can take before a warning is output to the log, in seconds */ public static final String databaseQueryMaxTimeProperty = "org.apache.manifoldcf.database.maxquerytime"; // Log configuration properties /** Location of log configuration file */ public static final String logConfigFileProperty = "org.apache.manifoldcf.logconfigfile"; // File resources property /** Location of file resources */ public static final String fileResourcesProperty = "org.apache.manifoldcf.fileresources"; // Implementation class properties /** Lock manager implementation class */ public static final String lockManagerImplementation = "org.apache.manifoldcf.lockmanagerclass"; /** Database implementation class */ public static final String databaseImplementation = "org.apache.manifoldcf.databaseimplementationclass"; /** Auth implementation class */ public static final String authImplementation = "org.apache.manifoldcf.authimplementationclass"; // The following are system integration properties /** Script to invoke when configuration changes, if any */ public static final String configSignalCommandProperty = "org.apache.manifoldcf.configuration.change.command"; /** File to look for to block access to UI during database maintenance */ public static final String maintenanceFileSignalProperty = "org.apache.manifoldcf.database.maintenanceflag"; /** Encryption salt property */ public static final String saltProperty = "org.apache.manifoldcf.salt"; /** Reset environment, minting a thread context for convenience and backwards * compatibility. */ @Deprecated public static void resetEnvironment() { resetEnvironment(ThreadContextFactory.make()); } /** Reset environment. */ public static void resetEnvironment(IThreadContext threadContext) { synchronized (initializeFlagLock) { if (initializeLevel > 0) { // Clean up the system doing the same thing the shutdown thread would have if the process was killed cleanUpEnvironment(threadContext); processID = null; mcfVersion = null; masterDatabaseName = null; masterDatabaseUsername = null; masterDatabasePassword = null; localConfiguration = null; propertyFilelastMod = -1L; propertyFilePath = null; alreadyClosed = false; alreadyShutdown = false; initializeLevel = 0; } } } /** Initialize environment, minting a thread context for backwards compatibility. */ @Deprecated public static void initializeEnvironment() throws ManifoldCFException { initializeEnvironment(ThreadContextFactory.make()); } /** Initialize environment. */ public static void initializeEnvironment(IThreadContext threadContext) throws ManifoldCFException { synchronized (initializeFlagLock) { if (initializeLevel == 0) { try { // Get system properties java.util.Properties props = System.getProperties(); // First, look for a define that might indicate where to look propertyFilePath = (String)props.get(lcfConfigFileProperty); if (propertyFilePath == null) { System.err.println("Couldn't find "+lcfConfigFileProperty+" property; using default"); String configPath = (String)props.get("user.home") + "/"+applicationName; configPath = configPath.replace('\\', '/'); propertyFilePath = new File(configPath,"properties.xml").toString(); } // Initialize working directory. We cannot use the actual system cwd, because different ManifoldCF processes will have different ones. // So, instead, we use the location of the property file itself, and call that the "working directory". workingDirectory = new File(propertyFilePath).getAbsoluteFile().getParentFile(); // Initialize resource loader. resourceLoader = new ManifoldCFResourceLoader(Thread.currentThread().getContextClassLoader()); // Read configuration! localConfiguration = new OverrideableManifoldCFConfiguration(); checkProperties(); // Process ID is always local processID = getStringProperty(processIDProperty,""); if (processID.length() > 16) throw new ManifoldCFException("Process ID cannot exceed 16 characters!"); // Log file is always local File logConfigFile = getFileProperty(logConfigFileProperty); if (logConfigFile == null) { System.err.println("Couldn't find "+logConfigFileProperty+" property; using default"); String configPath = (String)props.get("user.home") + "/"+applicationName; configPath = configPath.replace('\\', '/'); logConfigFile = new File(configPath,"logging.xml"); } // Make sure that the registered entry points for polling and cleanup are cleared, just in case. // This prevents classloader-style registration, which is actually not a good one for MCF architecture. synchronized (cleanupHooks) { cleanupHooks.clear(); } synchronized (pollingHooks) { pollingHooks.clear(); } Logging.initializeLoggingSystem(logConfigFile); // Set up local loggers Logging.initializeLoggers(); Logging.setLogLevels(threadContext); mcfVersion = LockManagerFactory.getStringProperty(threadContext,versionProperty,"unknown version"); masterDatabaseName = LockManagerFactory.getStringProperty(threadContext,masterDatabaseNameProperty,"dbname"); masterDatabaseUsername = LockManagerFactory.getStringProperty(threadContext,masterDatabaseUsernameProperty,"manifoldcf"); masterDatabasePassword = LockManagerFactory.getPossiblyObfuscatedStringProperty(threadContext,masterDatabasePasswordProperty,"local_pg_passwd"); // Register the connector services registerConnectorServices(); // Put the cache manager in the polling loop addPollingHook(new CachePoll()); // Register the file tracker for cleanup on shutdown tracker = new FileTrack(); addShutdownHook(tracker); // Register the database cleanup hook addShutdownHook(new DatabaseShutdown()); // Open the database. Done once per JVM. DBInterfaceFactory.make(threadContext,masterDatabaseName,masterDatabaseUsername,masterDatabasePassword).openDatabase(); } catch (ManifoldCFException e) { throw new ManifoldCFException("Initialization failed: "+e.getMessage(),e,ManifoldCFException.SETUP_ERROR); } } initializeLevel++; } } /** Register connector services provided in connectors and connector-commons */ protected static void registerConnectorServices() throws ManifoldCFException { try { Class connectorServicesManifoldCF = findClass("org.apache.manifoldcf.connectorcommon.system.ManifoldCF"); Method m = connectorServicesManifoldCF.getMethod("registerConnectorServices",new Class[0]); m.invoke(new Object[0]); } catch (ClassNotFoundException e) { Logging.root.warn("Could not find connectorcommon main class: "+e.getMessage(),e); } catch (NoSuchMethodException e) { Logging.root.warn("ManifoldCF.registerConnectorServices not found: "+e.getMessage(),e); } catch (IllegalAccessException e) { Logging.root.warn("Connectorcommon main class had illegal access: "+e.getMessage(),e); } catch (InvocationTargetException e) { Throwable z = e.getTargetException(); if (z instanceof Error) throw (Error)z; else if (z instanceof RuntimeException) throw (RuntimeException)z; else throw new RuntimeException("Unknown exception type: "+z.getClass().getName()+": "+z.getMessage(),z); } } /** For local properties (not shared!!), this class allows them to be overridden directly from the command line. */ protected static class OverrideableManifoldCFConfiguration extends ManifoldCFConfiguration { public OverrideableManifoldCFConfiguration() { super(); } @Override public String getProperty(String s) { String rval = System.getProperty(s); if (rval == null) rval = super.getProperty(s); return rval; } } /** Get process ID */ public static final String getProcessID() { return processID; } /** Get current properties. Makes no attempt to reread or interpret them. */ public static final ManifoldCFConfiguration getConfiguration() { return localConfiguration; } /** Reloads properties as needed. */ public static final void checkProperties() throws ManifoldCFException { File f = new File(propertyFilePath); // for re-read try { if (propertyFilelastMod != f.lastModified()) { InputStream is = new FileInputStream(f); try { localConfiguration.fromXML(is); System.err.println("Configuration file successfully read"); propertyFilelastMod = f.lastModified(); } finally { is.close(); } } else { System.err.println("Configuration file not read because it didn't change"); return; } } catch (Exception e) { throw new ManifoldCFException("Could not read configuration file '"+f.toString()+"'",e); } // For convenience, post-process all "lib" nodes. ArrayList libDirs = new ArrayList(); int i = 0; while (i < localConfiguration.getChildCount()) { ConfigurationNode cn = localConfiguration.findChild(i++); if (cn.getType().equals(NODE_LIBDIR)) { String path = cn.getAttributeValue(ATTRIBUTE_PATH); if (path == null) throw new ManifoldCFException("Node type '"+NODE_LIBDIR+"' requires a '"+ATTRIBUTE_PATH+" attribute"); // What exactly should I do with this classpath information? The classloader can be dynamically updated, but if I do that will everything work? // I'm going to presume the answer is "yes" for now... libDirs.add(resolvePath(path)); } } // Apply libdirs to the resource loader. resourceLoader.setClassPath(libDirs); } /** Resolve a file path, possibly relative to ManifoldCF's concept of its "working directory". *@param path is the path, to be calculated relative to the ManifoldCF "working directory". *@return the resolved file. */ public static File resolvePath(String path) { File r = new File(path); return r.isAbsolute() ? r : new File(workingDirectory, path); } /** Read a (string) property, either from the system properties, or from the local configuration file. *@param s is the property name. *@return the property value, as a string. */ public static String getProperty(String s) { return localConfiguration.getProperty(s); } /** Read a File property, either from the system properties, or from the local configuration file. * Relative file references are resolved according to the "working directory" for ManifoldCF. */ public static File getFileProperty(String s) { String value = getProperty(s); if (value == null) return null; return resolvePath(value); } /** Read a (string) property, either from the system properties, or from the local configuration file. *@param s is the property name. *@param defaultValue is the default value for the property. *@return the property value, as a string. */ public static String getStringProperty(String s, String defaultValue) { return localConfiguration.getStringProperty(s, defaultValue); } /** Read a boolean property */ public static boolean getBooleanProperty(String s, boolean defaultValue) throws ManifoldCFException { return localConfiguration.getBooleanProperty(s, defaultValue); } /** Read an integer property, either from the system properties, or from the local configuration file. */ public static int getIntProperty(String s, int defaultValue) throws ManifoldCFException { return localConfiguration.getIntProperty(s, defaultValue); } /** Read a long property, either from the system properties, or from the local configuration file. */ public static long getLongProperty(String s, long defaultValue) throws ManifoldCFException { return localConfiguration.getLongProperty(s, defaultValue); } /** Read a float property, either from the system properties, or from the local configuration file. */ public static double getDoubleProperty(String s, double defaultValue) throws ManifoldCFException { return localConfiguration.getDoubleProperty(s, defaultValue); } /** Attempt to make sure a path is a folder * @param path */ public static void ensureFolder(String path) throws ManifoldCFException { try { File f = new File(path); if (!f.isDirectory()) { f.mkdirs(); } } catch (Exception e) { throw new ManifoldCFException("Can't make folder",e,ManifoldCFException.GENERAL_ERROR); } } /** Delete a folder path. *@param path is the folder path. */ public static void deleteFolder(String path) { File directoryPath = new File(path); recursiveDelete(directoryPath); } /** Recursive delete: for cleaning up company folder. *@param directoryPath is the File describing the directory or file to be removed. */ public static void recursiveDelete(File directoryPath) { if (!directoryPath.exists()) return; if (directoryPath.isDirectory()) { File[] children = directoryPath.listFiles(); if (children != null) { int i = 0; while (i < children.length) { File x = children[i++]; recursiveDelete(x); } } } directoryPath.delete(); } /** Discover if a path is a folder * @param path spec, 'unix' form mostly */ public static boolean isFolder(String path) { File f = new File(path); return f.isDirectory(); } /** Convert a string into a safe, unique filename. *@param value is the string. *@return the file name. */ public static String safeFileName(String value) { StringBuilder rval = new StringBuilder(); int i = 0; while (i < value.length()) { char x = value.charAt(i++); if (x == '/' || x == '"' || x == '\\' || x == '|' || (x >= 0 && x < ' ') || x == '+' || x == ',' || x == ':' || x == ';' || x == '<' || x == '>' || x == '=' || x == '[' || x == ']' || x == '&') { // Stuff the character rval.append("&").append(Integer.toString((int)x)).append("!"); } else rval.append(x); } return rval.toString(); } /** Get the mcf version. *@return the version string */ public static String getMcfVersion() { return mcfVersion; } /** Get the master database name. *@return the master database name */ public static String getMasterDatabaseName() { return masterDatabaseName; } /** Get the master database username. *@return the master database username. */ public static String getMasterDatabaseUsername() { return masterDatabaseUsername; } /** Get the master database password. *@return the master database password. */ public static String getMasterDatabasePassword() { return masterDatabasePassword; } /** Find a child database name given a company database instance and the child * database identifier. *@param companyDatabase is the company database. *@param childDBIdentifier is the identifier. *@return the child database name. */ public static String getChildDatabaseName(IDBInterface companyDatabase, String childDBIdentifier) { return companyDatabase.getDatabaseName()+"_"+childDBIdentifier; } /** Perform standard hashing of a string * @param input is the string to hash. * @return the encrypted string. * */ public static String hash(String input) throws ManifoldCFException { MessageDigest hash = startHash(); addToHash(hash,input); return getHashValue(hash); } /** Start creating a hash */ public static MessageDigest startHash() throws ManifoldCFException { try { return MessageDigest.getInstance("SHA"); } catch (Exception e) { throw new ManifoldCFException("Couldn't encrypt: "+e.getMessage(),e,ManifoldCFException.GENERAL_ERROR); } } /** Add to hash */ public static void addToHash(MessageDigest digest, String input) throws ManifoldCFException { try { byte[] inputBytes = input.getBytes(StandardCharsets.UTF_8); digest.update(inputBytes); } catch (Exception e) { throw new ManifoldCFException("Couldn't encrypt: "+e.getMessage(),e,ManifoldCFException.GENERAL_ERROR); } } /** Calculate final hash value */ public static String getHashValue(MessageDigest digest) throws ManifoldCFException { try { byte[] encryptedBytes = digest.digest(); StringBuilder rval = new StringBuilder(); int i = 0; while (i < encryptedBytes.length) { byte x = encryptedBytes[i++]; rval.append(writeNibble((((int)x) >> 4) & 15)); rval.append(writeNibble(((int)x) & 15)); } return rval.toString(); } catch (Exception e) { throw new ManifoldCFException("Couldn't encrypt: "+e.getMessage(),e,ManifoldCFException.GENERAL_ERROR); } } protected static final int IV_LENGTH = 16; protected static String getSaltValue(IThreadContext threadContext) throws ManifoldCFException { final String saltValue = LockManagerFactory.getProperty(threadContext, saltProperty); if (saltValue == null || saltValue.length() == 0) throw new ManifoldCFException("Missing required SALT value"); return saltValue; } protected static Cipher getCipher(IThreadContext threadContext, final int mode, final String passCode, final byte[] iv) throws ManifoldCFException { return getCipher(getSaltValue(threadContext), mode, passCode, iv); } protected static Cipher getCipher(String saltValue, final int mode, final String passCode, final byte[] iv) throws ManifoldCFException { try { SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); KeySpec keySpec = new PBEKeySpec(passCode.toCharArray(), saltValue.getBytes(StandardCharsets.UTF_8), 1024, 128); SecretKey secretKey = factory.generateSecret(keySpec); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); SecretKeySpec key = new SecretKeySpec(secretKey.getEncoded(), "AES"); IvParameterSpec parameterSpec = new IvParameterSpec(iv); cipher.init(mode, key, parameterSpec); return cipher; } catch (GeneralSecurityException gse) { throw new ManifoldCFException("Could not build a cipher: " + gse.getMessage(),gse); } } protected static byte[] getSecureRandom() { SecureRandom random = new SecureRandom(); byte[] iv = new byte[IV_LENGTH]; random.nextBytes(iv); return iv; } private static String OBFUSCATION_PASSCODE = "NowIsTheTime"; private static String OBFUSCATION_SALT = "Salty"; /** Encode a string in a reversible obfuscation. *@param input is the input string. *@return the output string. */ public static String obfuscate(String input) throws ManifoldCFException { return encrypt(OBFUSCATION_SALT, OBFUSCATION_PASSCODE, input); } /** Decode a string encoded using the obfuscation * technique. *@param input is the input string. *@return the decoded string. */ public static String deobfuscate(String input) throws ManifoldCFException { return decrypt(OBFUSCATION_SALT, OBFUSCATION_PASSCODE, input); } /** Encrypt a string in a reversible encryption. *@param saltValue is the salt value. *@param passCode is the pass code. *@param input is the input string. *@return the output string. */ public static String encrypt(String saltValue, String passCode, String input) throws ManifoldCFException { if (input == null) return null; if (input.length() == 0) return input; try { ByteArrayOutputStream os = new ByteArrayOutputStream(); // Write IV as a prefix: byte[] iv = getSecureRandom(); os.write(iv); os.flush(); Cipher cipher = getCipher(saltValue, Cipher.ENCRYPT_MODE, passCode, iv); CipherOutputStream cos = new CipherOutputStream(os, cipher); Writer w = new OutputStreamWriter(cos,java.nio.charset.StandardCharsets.UTF_8); w.write(input); w.flush(); // These two shouldn't be necessary, but they are. cos.flush(); cos.close(); byte[] bytes = os.toByteArray(); return new org.apache.manifoldcf.core.common.Base64().encodeByteArray(bytes); } catch (IOException e) { throw new ManifoldCFException(e.getMessage(),e); } } /** Decrypt a string. *@param saltValue is the salt value. *@param passCode is the pass code. *@param input is the input string. *@return the decoded string. */ public static String decrypt(String saltValue, String passCode, String input) throws ManifoldCFException { if (input == null) return null; if (input.length() == 0) return input; try { ByteArrayInputStream is = new ByteArrayInputStream(new org.apache.manifoldcf.core.common.Base64().decodeString(input)); byte[] iv = new byte[IV_LENGTH]; int pointer = 0; while (pointer < iv.length) { int amt = is.read(iv,pointer,iv.length-pointer); if (amt == -1) throw new ManifoldCFException("String can't be decrypted: too short"); pointer += amt; } Cipher cipher = getCipher(saltValue, Cipher.DECRYPT_MODE, passCode, iv); CipherInputStream cis = new CipherInputStream(is, cipher); InputStreamReader reader = new InputStreamReader(cis,java.nio.charset.StandardCharsets.UTF_8); StringBuilder sb = new StringBuilder(); char[] buffer = new char[65536]; while (true) { int amt = reader.read(buffer,0,buffer.length); if (amt == -1) break; sb.append(buffer,0,amt); } return sb.toString(); } catch (IOException e) { throw new ManifoldCFException(e.getMessage(),e); } } /** Encode a string in a reversible obfuscation. *@param input is the input string. *@return the output string. */ /* public static String obfuscate(String input) throws ManifoldCFException { if (input == null) return null; if (input.length() == 0) return input; // First, convert to binary byte[] array = input.getBytes(StandardCharsets.UTF_8); // Shift and xor // We shift by some number not a multiple of 4. // The resulting hexadecimal is then not a simple shift. int i = 0; int carryover = (((int)array[array.length-1]) & 0x1f); while (i < array.length) { int x = (int)array[i]; int newCarryover = x & 0x1f; x = ((x >> 5) & 0x7) + (carryover << 3); carryover = newCarryover; array[i++] = (byte)(x ^ 0x59); } // Now, convert to hex StringBuilder rval = new StringBuilder(); i = 0; while (i < array.length) { int x = (int)array[i++]; rval.append(writeNibble((x >> 4) & 0x0f)); rval.append(writeNibble(x & 0x0f)); } return rval.toString(); } */ /** Write a hex nibble. *@param value is the value to write. *@return the character. */ protected static char writeNibble(int value) { if (value >= 10) return (char)(value-10+'A'); else return (char)(value+'0'); } /** Decode a string encoded using the obfuscation * technique. *@param input is the input string. *@return the decoded string. */ /* public static String deobfuscate(String input) throws ManifoldCFException { if (input == null) return null; if (input.length() == 0) return input; if ((input.length() >> 1) * 2 != input.length()) throw new ManifoldCFException("Decoding error",ManifoldCFException.GENERAL_ERROR); byte[] bytes = new byte[input.length() >> 1]; int i = 0; int j = 0; while (i < input.length()) { int x0 = readNibble(input.charAt(i++)); int x1 = readNibble(input.charAt(i++)); int x = (x0 << 4) + x1; bytes[j++] = (byte)x; } // Process the array in reverse order int carryover = ((((int)bytes[0]) ^ 0x59) >> 3) & 0x1f; i = bytes.length; while (i > 0) { i--; int x = ((int)bytes[i]) ^ 0x59; int newCarryover = (x >> 3) & 0x1f; x = (x << 5) + carryover; bytes[i] = (byte)x; carryover = newCarryover; } // Convert from utf-8 to a string return new String(bytes,StandardCharsets.UTF_8); } */ /** Read a hex nibble. *@param value is the character. *@return the value. */ protected static int readNibble(char value) throws ManifoldCFException { if (value >= 'A' && value <= 'F') return (int)(value - 'A' + 10); else if (value >= '0' && value <= '9') return (int)(value - '0'); else throw new ManifoldCFException("Bad hexadecimal value",ManifoldCFException.GENERAL_ERROR); } /** Install system database. *@param threadcontext is the thread context. *@param masterUsername is the master database user name. *@param masterPassword is the master database password. */ public static void createSystemDatabase(IThreadContext threadcontext, String masterUsername, String masterPassword) throws ManifoldCFException { String databaseName = getMasterDatabaseName(); String databaseUsername = getMasterDatabaseUsername(); String databasePassword = getMasterDatabasePassword(); IDBInterface master = DBInterfaceFactory.make(threadcontext,databaseName,databaseUsername,databasePassword); master.createUserAndDatabase(masterUsername,masterPassword,null); } /** Drop system database. *@param threadcontext is the thread context. *@param masterUsername is the master database user name. *@param masterPassword is the master database password. */ public static void dropSystemDatabase(IThreadContext threadcontext, String masterUsername, String masterPassword) throws ManifoldCFException { String databaseName = getMasterDatabaseName(); String databaseUsername = getMasterDatabaseUsername(); String databasePassword = getMasterDatabasePassword(); IDBInterface master = DBInterfaceFactory.make(threadcontext,databaseName,databaseUsername,databasePassword); master.dropUserAndDatabase(masterUsername,masterPassword,null); } /** Create temporary directory. */ public static File createTempDir(String prefix, String suffix) throws ManifoldCFException { String tempDirLocation = System.getProperty("java.io.tmpdir"); if (tempDirLocation == null) throw new ManifoldCFException("Can't find temporary directory!"); File tempDir = new File(tempDirLocation); // Start with current timestamp, and generate a hash, then look for collision long currentFileID = System.currentTimeMillis(); long currentFileHash = (currentFileID << 5) ^ (currentFileID >> 3); int raceConditionRepeat = 0; while (raceConditionRepeat < 1000) { File tempCertDir = new File(tempDir,prefix+currentFileHash+suffix); if (tempCertDir.mkdir()) { return tempCertDir; } if (tempCertDir.exists()) { currentFileHash++; continue; } // Doesn't exist but couldn't create either. COULD be a race condition; we'll only know if we retry // lots and nothing changes. raceConditionRepeat++; Thread.yield(); } throw new ManifoldCFException("Temporary directory appears to be unwritable"); } /** Add a file to the tracking system. */ public static void addFile(File f) { tracker.addFile(f); } /** Use the tracking system to delete a file. You MUST use this to * delete any file that was added to the tracking system with addFile(). */ public static void deleteFile(File f) { tracker.deleteFile(f); } /** Check if maintenance is underway. */ public static boolean checkMaintenanceUnderway() { // File check is always local; this whole bit of logic needs to be rethought though. String fileToCheck = getProperty(maintenanceFileSignalProperty); if (fileToCheck != null && fileToCheck.length() > 0) { File f = new File(fileToCheck); return f.exists(); } return false; } /** Note configuration change. */ public static void noteConfigurationChange() throws ManifoldCFException { // Always a local file. This needs to be rethought how it should operate in a clustered world. String configChangeSignalCommand = getProperty(configSignalCommandProperty); if (configChangeSignalCommand == null || configChangeSignalCommand.length() == 0) return; // Do stuff to the file to note change. This involves // shelling out to the os and involving whatever is desired. // We should try to convert the command into arguments. ArrayList list = new ArrayList(); int currentIndex = 0; while (currentIndex < configChangeSignalCommand.length()) { // Suck up the leading whitespace while (currentIndex < configChangeSignalCommand.length()) { char x = configChangeSignalCommand.charAt(currentIndex); if (x < 0 || x > ' ') break; currentIndex++; } StringBuilder argBuffer = new StringBuilder(); boolean isQuoted = false; while (currentIndex < configChangeSignalCommand.length()) { char x = configChangeSignalCommand.charAt(currentIndex); if (isQuoted) { if (x == '"') { currentIndex++; isQuoted = false; } else if (x == '\\') { currentIndex++; if (currentIndex < configChangeSignalCommand.length()) { x = configChangeSignalCommand.charAt(currentIndex); argBuffer.append(x); } else break; } else { currentIndex++; argBuffer.append(x); } } else { if (x == '"') { currentIndex++; isQuoted = true; } else if (x == '\\') { currentIndex++; if (currentIndex < configChangeSignalCommand.length()) { x = configChangeSignalCommand.charAt(currentIndex); argBuffer.append(x); } else break; } else if (x >= 0 && x <= ' ') break; else { currentIndex++; argBuffer.append(x); } } } list.add(argBuffer.toString()); } // Set up for command invocation String[] commandArray = new String[list.size()]; int i = 0; while (i < commandArray.length) { commandArray[i] = (String)list.get(i); i++; } if (commandArray.length == 0) return; String[] env = new String[0]; File dir = new File("/"); try { // Do the exec. Process p = Runtime.getRuntime().exec(commandArray,env,dir); try { // To make this truly "safe", we really ought to spin up a thread to handle both the standard error and the standard output streams - otherwise // we run the risk of getting blocked here. In practice, there's enough buffering in the OS to handle what we need right now. int rval = p.waitFor(); if (rval != 0) { InputStream is = p.getErrorStream(); try { Reader r = new InputStreamReader(is, StandardCharsets.UTF_8); try { BufferedReader br = new BufferedReader(r); try { StringBuilder sb = new StringBuilder(); while (true) { String value = br.readLine(); if (value == null) break; sb.append(value).append("; "); } throw new ManifoldCFException("Shelled process '"+configChangeSignalCommand+"' failed with error "+Integer.toString(rval)+": "+sb.toString()); } finally { br.close(); } } finally { r.close(); } } finally { is.close(); } } } finally { p.destroy(); } } catch (InterruptedException e) { throw new ManifoldCFException("Process wait interrupted: "+e.getMessage(),e,ManifoldCFException.INTERRUPTED); } catch (InterruptedIOException e) { throw new ManifoldCFException("IO with subprocess interrupted: "+e.getMessage(),e,ManifoldCFException.INTERRUPTED); } catch (IOException e) { throw new ManifoldCFException("IO exception signalling change: "+e.getMessage(),e); } } /** Use this method to sleep instead of Thread.sleep(). Thread.sleep() doesn't seem to work well when the system * time is reset. */ public static void sleep(long milliseconds) throws InterruptedException { // Unfortunately we need to create an object for every time that we sleep Integer x = new Integer(0); synchronized (x) { x.wait(milliseconds); } } /** Write a bunch of bytes to the output stream */ public static void writeBytes(OutputStream os, byte[] byteArray) throws IOException { os.write(byteArray, 0, byteArray.length); } /** Write a byte to an output stream */ public static void writeByte(OutputStream os, int byteValue) throws IOException { writeBytes(os,new byte[]{(byte)byteValue}); } /** Write a word to an output stream */ public static void writeWord(OutputStream os, int wordValue) throws IOException { byte[] buffer = new byte[2]; buffer[0] = (byte)(wordValue & 0xff); buffer[1] = (byte)((wordValue >>> 8) & 0xff); writeBytes(os,buffer); } /** Write a dword to an output stream */ public static void writeDword(OutputStream os, int dwordValue) throws IOException { if (dwordValue < 0) throw new IllegalArgumentException("Attempt to use an unsigned operator to write a signed value"); writeSdword(os,dwordValue); } /** Write a signed dword to an output stream */ public static void writeSdword(OutputStream os, int dwordValue) throws IOException { byte[] buffer = new byte[4]; buffer[0] = (byte)(dwordValue & 0xff); buffer[1] = (byte)((dwordValue >>> 8) & 0xff); buffer[2] = (byte)((dwordValue >>> 16) & 0xff); buffer[3] = (byte)((dwordValue >>> 24) & 0xff); writeBytes(os, buffer); } /** Write a Long to an output stream */ public static void writeLong(OutputStream os, Long longValue) throws IOException { if (longValue == null) writeByte(os,1); else { writeByte(os,0); long value = longValue.longValue(); byte[] buffer = new byte[8]; buffer[0] = (byte)(value & 0xff); buffer[1] = (byte)(Long.rotateRight(value,8) & 0xff); buffer[2] = (byte)(Long.rotateRight(value,16) & 0xff); buffer[3] = (byte)(Long.rotateRight(value,24) & 0xff); buffer[4] = (byte)(Long.rotateRight(value,32) & 0xff); buffer[5] = (byte)(Long.rotateRight(value,40) & 0xff); buffer[6] = (byte)(Long.rotateRight(value,48) & 0xff); buffer[7] = (byte)(Long.rotateRight(value,56) & 0xff); writeBytes(os,buffer); } } /** Write a String to an output stream */ public static void writeString(OutputStream os, String stringValue) throws IOException { byte[] characters; if (stringValue == null) characters = null; else characters = stringValue.getBytes(StandardCharsets.UTF_8); writeByteArray(os, characters); } /** Write a byte array to an output stream */ public static void writeByteArray(OutputStream os, byte[] byteArray) throws IOException { if (byteArray == null) writeSdword(os,-1); else { writeSdword(os,byteArray.length); writeBytes(os,byteArray); } } /** Write a float value to an output stream */ public static void writefloat(OutputStream os, float floatValue) throws IOException { writeSdword(os, Float.floatToIntBits(floatValue)); } /** Read bytes from the input stream into specified array. */ public static void readBytes(InputStream is, byte[] byteArray) throws IOException { int amtSoFar = 0; while (amtSoFar < byteArray.length) { int amt = is.read(byteArray,amtSoFar,byteArray.length-amtSoFar); if (amt == -1) throw new IOException("Unexpected EOF"); amtSoFar += amt; } } /** Read a byte from an input stream */ public static int readByte(InputStream is) throws IOException { byte[] inputArray = new byte[1]; readBytes(is,inputArray); return ((int)inputArray[0]) & 0xff; } /** Read a word from an input stream */ public static int readWord(InputStream is) throws IOException { byte[] inputArray = new byte[2]; readBytes(is,inputArray); return (((int)inputArray[0]) & 0xff) + ((((int)inputArray[1]) & 0xff) << 8); } /** Read a dword from an input stream */ public static int readDword(InputStream is) throws IOException { byte[] inputArray = new byte[4]; readBytes(is,inputArray); return (((int)inputArray[0]) & 0xff) + ((((int)inputArray[1]) & 0xff) << 8) + ((((int)inputArray[2]) & 0xff) << 16) + ((((int)inputArray[3]) & 0xff) << 24); } /** Read a signed dword from an input stream */ public static int readSdword(InputStream is) throws IOException { byte[] inputArray = new byte[4]; readBytes(is,inputArray); return (((int)inputArray[0]) & 0xff) + ((((int)inputArray[1]) & 0xff) << 8) + ((((int)inputArray[2]) & 0xff) << 16) + (((int)inputArray[3]) << 24); } /** Read a Long from an input stream */ public static Long readLong(InputStream is) throws IOException { int value = readByte(is); if (value == 1) return null; byte[] inputArray = new byte[8]; readBytes(is,inputArray); return new Long((long)(((int)inputArray[0]) & 0xff) + Long.rotateLeft(((int)inputArray[1]) & 0xff, 8) + Long.rotateLeft(((int)inputArray[2]) & 0xff, 16) + Long.rotateLeft(((int)inputArray[3]) & 0xff, 24) + Long.rotateLeft(((int)inputArray[4]) & 0xff, 32) + Long.rotateLeft(((int)inputArray[5]) & 0xff, 40) + Long.rotateLeft(((int)inputArray[6]) & 0xff, 48) + Long.rotateLeft(((int)inputArray[7]) & 0xff, 56)); } /** Read a String from an input stream */ public static String readString(InputStream is) throws IOException { byte[] bytes = readByteArray(is); if (bytes == null) return null; return new String(bytes,StandardCharsets.UTF_8); } /** Read a byte array from an input stream */ public static byte[] readByteArray(InputStream is) throws IOException { int length = readSdword(is); if (length == -1) return null; byte[] byteArray = new byte[length]; readBytes(is,byteArray); return byteArray; } /** Read a float value from an input stream */ public static float readfloat(InputStream os) throws IOException { return Float.intBitsToFloat(readSdword(os)); } /** Add a cleanup hook to the list. These hooks will be evaluated in the * reverse order than the order in which they were added. *@param hook is the shutdown hook that needs to be added to the sequence. */ public static void addShutdownHook(IShutdownHook hook) { synchronized (cleanupHooks) { cleanupHooks.add(hook); } } /** Add a polling hook to the list. These hooks will be evaluated in the * order they were added. *@param hook is the polling hook that needs to be added to the sequence. */ public static void addPollingHook(IPollingHook hook) { synchronized (pollingHooks) { pollingHooks.add(hook); } } /** Poll all the registered polling services. */ public static void pollAll(IThreadContext threadContext) throws ManifoldCFException { synchronized (pollingHooks) { for (IPollingHook hook : pollingHooks) { hook.doPoll(threadContext); } } } /** Create a new resource loader based on the default one. This is used by * connectors wishing to make their own resource loaders for isolation purposes. */ public static ManifoldCFResourceLoader createResourceLoader() throws ManifoldCFException { return new ManifoldCFResourceLoader(resourceLoader.getClassLoader()); } /** Locate a class in the configuration-determined class path. This method * is designed for loading plugin classes, and their downstream dependents. */ public static Class findClass(String cname) throws ClassNotFoundException,ManifoldCFException { return resourceLoader.findClass(cname); } /** Perform system shutdown, minting thread context for backwards compatibility */ @Deprecated public static void cleanUpEnvironment() { cleanUpEnvironment(ThreadContextFactory.make()); } /** Perform system shutdown, using the registered shutdown hooks. */ public static void cleanUpEnvironment(IThreadContext threadContext) { synchronized (initializeFlagLock) { initializeLevel--; // It needs to call all registered shutdown hooks, in reverse order. // A failure of any one hook should cause the cleanup to continue, after a logging attempt is made. if (initializeLevel == 0 && !alreadyShutdown) { synchronized (cleanupHooks) { int i = cleanupHooks.size(); while (i > 0) { i--; IShutdownHook hook = cleanupHooks.get(i); try { hook.doCleanup(threadContext); } catch (ManifoldCFException e) { Logging.root.warn("Error during system shutdown: "+e.getMessage(),e); } } cleanupHooks.clear(); } synchronized (pollingHooks) { pollingHooks.clear(); } alreadyShutdown = true; } } } /** Class that tracks files that need to be cleaned up on exit */ protected static class FileTrack implements IShutdownHook { /** Set of File objects */ protected Set<File> filesToDelete = new HashSet<File>(); /** Constructor */ public FileTrack() { } /** Add a file to track */ public void addFile(File f) { synchronized (this) { filesToDelete.add(f); } } /** Delete a file */ public void deleteFile(File f) { // Because we never reuse file names, it is OK to delete twice. // So the delete() can be outside the synchronizer. recursiveDelete(f); synchronized (this) { filesToDelete.remove(f); } } /** Delete all remaining files */ @Override public void doCleanup(IThreadContext threadContext) throws ManifoldCFException { synchronized (this) { Iterator<File> iter = filesToDelete.iterator(); while (iter.hasNext()) { File f = iter.next(); f.delete(); } filesToDelete.clear(); } } /** Finalizer, which is designed to catch class unloading that tomcat 5.5 does. */ protected void finalize() throws Throwable { try { doCleanup(ThreadContextFactory.make()); } finally { super.finalize(); } } } /** Class that cleans up expired cache objects on polling. */ protected static class CachePoll implements IPollingHook { public CachePoll() { } @Override public void doPoll(IThreadContext threadContext) throws ManifoldCFException { ICacheManager cacheManager = CacheManagerFactory.make(threadContext); cacheManager.expireObjects(System.currentTimeMillis()); } } /** Class that cleans up database handles on exit */ protected static class DatabaseShutdown implements IShutdownHook { public DatabaseShutdown() { } @Override public void doCleanup(IThreadContext threadContext) throws ManifoldCFException { // Clean up the database handles Thread t = new DatabaseConnectionReleaseThread(); t.start(); try { // Wait 15 seconds for database cleanup to finish. If we haven't managed to close database connections by then, we give up and just exit. t.join(15000L); } catch (InterruptedException e) { } closeDatabase(); } protected void closeDatabase() throws ManifoldCFException { synchronized (initializeFlagLock) { if (initializeLevel == 0 && !alreadyClosed) { IThreadContext threadcontext = ThreadContextFactory.make(); String databaseName = getMasterDatabaseName(); String databaseUsername = getMasterDatabaseUsername(); String databasePassword = getMasterDatabasePassword(); DBInterfaceFactory.make(threadcontext,databaseName,databaseUsername,databasePassword).closeDatabase(); alreadyClosed = true; } } } /** Finalizer, which is designed to catch class unloading that tomcat 5.5 does. */ protected void finalize() throws Throwable { try { // The database handle cleanup is handled inside the finalizers for the pools that hold onto connections. closeDatabase(); } finally { super.finalize(); } } } /** Finisher thread, to be registered with the runtime */ protected static class ShutdownThread extends Thread { /** Constructor. */ public ShutdownThread() { super(); setName("Shutdown thread"); } public void run() { // This thread is run at shutdown time. cleanUpEnvironment(ThreadContextFactory.make()); } } /** The thread that actually releases database connections */ protected static class DatabaseConnectionReleaseThread extends Thread { /** Constructor. */ public DatabaseConnectionReleaseThread() { super(); setName("Database connection release thread"); // May be abandoned if it takes too long setDaemon(true); } public void run() { // Clean up the database handles org.apache.manifoldcf.core.database.ConnectionFactory.releaseAll(); } } }