/* GNU GENERAL PUBLIC LICENSE Copyright (C) 2006 The Lobo Project This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either verion 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Contact info: lobochief@users.sourceforge.net */ /* * Created on May 31, 2005 */ package org.lobobrowser.store; import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.Serializable; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.lobobrowser.io.ManagedFile; import org.lobobrowser.io.ManagedFileFilter; import org.lobobrowser.io.ManagedStore; import org.lobobrowser.io.QuotaExceededException; import org.lobobrowser.util.WrapperException; public final class RestrictedStore implements QuotaSource, ManagedStore { private static final Logger logger = Logger.getLogger(RestrictedStore.class.getName()); /** * Canonical base directory. */ private final File baseDirectory; private final String baseCanonicalPath; private final String sizeFileCanonicalPath; private final long quota; private final String SIZE_FILE_NAME = ".W$Dir$Size"; /** Made up **/ private final int EMPTY_FILE_SIZE = 64; /** Made up **/ private final int DIRECTORY_SIZE = 64; private long size = -1; /** * */ public RestrictedStore(final File baseDirectory, final long quota) throws IOException { // Security: This constructor is only allowed to be invoked // by a caller with privileged access to the directory. final SecurityManager sm = System.getSecurityManager(); final String canonical = baseDirectory.getCanonicalPath(); if (sm != null) { sm.checkWrite(canonical); } if (!baseDirectory.exists()) { baseDirectory.mkdirs(); } else if (!baseDirectory.isDirectory()) { throw new IllegalArgumentException(baseDirectory + " not a directory"); } this.baseDirectory = new File(canonical); this.baseCanonicalPath = canonical; this.sizeFileCanonicalPath = new File(this.baseDirectory, SIZE_FILE_NAME).getCanonicalPath(); this.quota = quota; } long updateSizeFile() throws IOException { final long totalSize = this.computeSize(); long prevSize; synchronized (this) { prevSize = this.size; this.updateSizeFileImpl(totalSize); } if ((prevSize != -1) && (Math.abs(totalSize - prevSize) > 10000)) { logger.warning("updateSizeFile(): Corrected a size discrepancy of " + (totalSize - prevSize) + " bytes in store '" + this.baseDirectory + "'."); } return totalSize; } private void updateSizeFileImpl(final long totalSize) throws IOException { // The computed size is not necessarily precise. That's // why we have this. synchronized (this) { this.size = totalSize; final File sizeFile = new File(this.baseDirectory, SIZE_FILE_NAME); try ( final FileOutputStream out = new FileOutputStream(sizeFile); final DataOutputStream dout = new DataOutputStream(out);) { dout.writeLong(totalSize); dout.flush(); } } } public long getQuota() { return this.quota; } public long getSize() throws IOException { try { return AccessController.doPrivileged(new PrivilegedAction<Long>() { public Long run() { synchronized (this) { try { long size = RestrictedStore.this.size; if (size == -1) { size = RestrictedStore.this.size = RestrictedStore.this.getSizeFromFile(); } return size; } catch (final IOException ioe) { throw new WrapperException(ioe); } } } }); } catch (final WrapperException we) { throw (IOException) we.getCause(); } } private long getSizeFromFile() throws IOException { final File sizeFile = new File(this.baseDirectory, SIZE_FILE_NAME); try { final FileInputStream in = new FileInputStream(sizeFile); try { final DataInputStream din = new DataInputStream(in); return din.readLong(); } finally { in.close(); } } catch (final java.io.FileNotFoundException fnf) { return this.updateSizeFile(); } } private long computeSize() throws IOException { return this.computeSize(this.baseDirectory); } private long computeSize(final File directory) throws IOException { if (!directory.isDirectory()) { throw new IllegalArgumentException("'directory' not a directory"); } long total = DIRECTORY_SIZE; final File[] files = directory.listFiles(); for (final File file : files) { Thread.yield(); if (file.isDirectory() && !file.equals(directory)) { final String fileCanonical = file.getCanonicalPath(); if (fileCanonical.startsWith(this.baseCanonicalPath)) { total += this.computeSize(file); } } else { total += (EMPTY_FILE_SIZE + file.length()); } } return total; } private long lastUpdatedSize = Long.MIN_VALUE; private static long SIZE_UPDATE_THRESHOLD = 4096; public void addUsedBytes(final long addition) throws IOException { synchronized (this) { // long size = this.getSize(); boolean fromFile = false; if (this.size == -1) { this.size = this.getSizeFromFile(); fromFile = true; } final long newTotal = this.size + addition; if ((addition > 0) && (newTotal > this.quota)) { throw new QuotaExceededException("Quota would be exceeded by " + (newTotal - this.quota) + " bytes."); } this.size = newTotal; if (fromFile) { this.lastUpdatedSize = newTotal; } else if (Math.abs(newTotal - this.lastUpdatedSize) > SIZE_UPDATE_THRESHOLD) { this.lastUpdatedSize = newTotal; this.updateSizeFileImpl(newTotal); } } } /* * (non-Javadoc) * * @see net.sourceforge.xamj.store.QuotaSource#addUsedBytes(long) */ public void subtractUsedBytes(final long reduction) throws IOException { this.addUsedBytes(-reduction); } private void checkNotSizeFile(final String canonicalPath, final String ref) { if (this.sizeFileCanonicalPath.equals(canonicalPath)) { throw new SecurityException("This particular path not allowed: " + ref); } } private void checkPath(final String canonicalPath, final String ref) { if (!canonicalPath.startsWith(this.baseCanonicalPath)) { throw new SecurityException("Path outside protected store: " + ref); } this.checkNotSizeFile(canonicalPath, ref); } public InputStream getInputStream(final File fullFile, final String ref) throws IOException { try { return AccessController.doPrivileged(new PrivilegedAction<InputStream>() { // Reason: Caller was able to get an instance of this // RestrictedStore. Additionally, we check that the File // path is within what's allowed. public InputStream run() { try { final String canonical = fullFile.getCanonicalPath(); checkPath(canonical, ref); return new FileInputStream(fullFile); } catch (final IOException ioe) { throw new WrapperException(ioe); } } }); } catch (final WrapperException we) { throw (IOException) we.getCause(); } } public OutputStream getOutputStream(final File fullFile, final String ref) throws IOException { try { return AccessController.doPrivileged(new PrivilegedAction<OutputStream>() { // Reason: Caller was able to get an instance of this // RestrictedStore. Additionally, we check that the File // path is within what's allowed. public OutputStream run() { try { final long toSubtract = EMPTY_FILE_SIZE + (fullFile.exists() ? fullFile.length() : 0); final String canonical = fullFile.getCanonicalPath(); checkPath(canonical, ref); // TODO: Disallow size file here final File parent = fullFile.getParentFile(); if (!parent.exists()) { parent.mkdirs(); } else if (!parent.isDirectory()) { throw new IllegalArgumentException("Parent of '" + ref + "' is not a directory"); } final FileOutputStream fout = new FileOutputStream(fullFile); final OutputStream out = new RestrictedOutputStream(fout, RestrictedStore.this); if (toSubtract != 0) { subtractUsedBytes(toSubtract); } return out; } catch (final IOException ioe) { throw new WrapperException(ioe); } } }); } catch (final WrapperException we) { throw (IOException) we.getCause(); } } private String getRelativePath(final String canonicalPath) { String relativePath = canonicalPath.substring(this.baseCanonicalPath.length()); if (relativePath.startsWith(File.separator)) { relativePath = relativePath.substring(File.separator.length()); } if (!"/".equals(File.separator)) { relativePath = relativePath.replace(File.separatorChar, '/'); } return relativePath; } public Collection<String> getPaths(final String regexp) throws IOException { final Pattern pattern = Pattern.compile(regexp); try { return AccessController.doPrivileged(new PrivilegedAction<Collection<String>>() { // Reason: Calling getPaths() requires certain file permissions // that the caller might not naturally have. Paths are relative to // the baseDirectory of the RestrictedStore. The user must have // proper hosts privileges to be able to get the RestrictedStore // instance. public Collection<String> run() { try { return getPaths(pattern, baseDirectory); } catch (final IOException ioe) { throw new WrapperException(ioe); } } }); } catch (final WrapperException we) { throw (IOException) we.getCause(); } } private Collection<String> getPaths(final Pattern pattern, final File directory) throws IOException { // Security: This method is expected to be private. final Collection<String> paths = new LinkedList<>(); final File[] localFiles = directory.listFiles(); for (final File file : localFiles) { if (file.isDirectory()) { final Collection<String> subPaths = this.getPaths(pattern, file); paths.addAll(subPaths); } else { final String canonical = file.getCanonicalPath(); final String relativePath = this.getRelativePath(canonical); final Matcher matcher = pattern.matcher(relativePath); if (matcher.matches()) { try { this.checkPath(canonical, "not-shown"); paths.add(relativePath); } catch (final SecurityException se) { // ignore file } } } } return paths; } /* * (non-Javadoc) * * @see net.sourceforge.xamj.store.QuotaSource#getSpaceLeft() */ public long getSpaceLeft() throws IOException { return this.quota - this.getSize(); } public void saveObject(final String path, final Serializable object) throws IOException { final ManagedFile file = this.getManagedFile(path); try ( final OutputStream out = file.openOutputStream()) { final ObjectOutputStream oout = new ObjectOutputStream(new BufferedOutputStream(out)); oout.writeObject(object); oout.flush(); } } public void removeObject(final String path) throws IOException { final ManagedFile file = this.getManagedFile(path); file.delete(); } public Object retrieveObject(final String path) throws IOException, ClassNotFoundException { return this.retrieveObject(path, Thread.currentThread().getContextClassLoader()); } public Object retrieveObject(final String path, final ClassLoader classLoader) throws IOException, ClassNotFoundException { final ManagedFile file = this.getManagedFile(path); try ( final InputStream in = file.openInputStream(); final ObjectInputStream oin = new ClassLoaderObjectInputStream(in, classLoader)) { return oin.readObject(); } catch (final FileNotFoundException err) { return null; } } /* * (non-Javadoc) * * @see org.xamjwg.io.ManagedStore#getManagedFile(org.xamjwg.io.ManagedFile, * java.lang.String) */ public ManagedFile getManagedFile(final ManagedFile parent, final String relativePath) throws IOException { return new ManagedFileImpl(parent, relativePath); } /* * (non-Javadoc) * * @see org.xamjwg.io.ManagedStore#getManagedFile(java.lang.String) */ public ManagedFile getManagedFile(final String path) throws IOException { return new ManagedFileImpl(path); } public ManagedFile getRootManagedDirectory() throws IOException { return new ManagedFileImpl("/"); } private File managedToNative(final String path) throws IOException { try { return AccessController.doPrivileged(new PrivilegedAction<File>() { public File run() { try { if (path.contains("\\")) { throw new IllegalArgumentException("Characer backslash (\\) not allowed in managed paths. Use a forward slash. Path=" + path); } String relPath = path; while (relPath.startsWith("/")) { relPath = relPath.substring(1); } relPath = relPath.replace("/", File.separator); File fullFile; if (relPath.length() == 0) { fullFile = baseDirectory; } else { fullFile = new File(baseDirectory, relPath); } final String canonical = fullFile.getCanonicalPath(); // Must check so that all ManagedFile instances // are known to be safe. checkPath(canonical, path); return fullFile; } catch (final IOException ioe) { throw new WrapperException(ioe); } } }); } catch (final WrapperException we) { throw (IOException) we.getCause(); } } private ManagedFile nativeToManaged(final File file) throws IOException { final String canonical = file.getCanonicalPath(); if (!canonical.startsWith(this.baseCanonicalPath)) { throw new SecurityException("File is outside of managed store"); } String mpath = canonical.substring(this.baseCanonicalPath.length()); if (!mpath.startsWith(File.separator)) { mpath = File.separator + mpath; } return new ManagedFileImpl(mpath); } private class ManagedFileImpl implements ManagedFile { // NOTE: ManagedFileImpl instances should only be allowed // to exist in association with a RestrictedStore. private final String path; private final File nativeFile; private ManagedFileImpl(final String path) throws IOException { this.path = path; // Note: managedToNative has a security check. this.nativeFile = managedToNative(path); } private ManagedFileImpl(final ManagedFile parent, final String relPath) throws IOException { if (parent == null) { this.path = relPath; } else { if (relPath.startsWith("/")) { this.path = relPath; } else { final String pp = parent.getPath(); if (pp.endsWith("/")) { this.path = pp + relPath; } else { this.path = pp + "/" + relPath; } } } // Note: managedToNative has a security check. this.nativeFile = managedToNative(this.path); } /* * (non-Javadoc) * * @see org.xamjwg.io.ManagedFile#createFile() */ public boolean createNewFile() throws IOException { try { return AccessController.doPrivileged(new PrivilegedAction<Boolean>() { // Reason: Should be allowed. Obtaining an instance of ManagedFileImpl // requires privileges. public Boolean run() { try { final boolean success = nativeFile.createNewFile(); if (success) { RestrictedStore.this.addUsedBytes(EMPTY_FILE_SIZE); } return success; } catch (final IOException ioe) { throw new WrapperException(ioe); } } }); } catch (final WrapperException we) { throw (IOException) we.getCause(); } } /* * (non-Javadoc) * * @see org.xamjwg.io.ManagedFile#exists() */ public boolean exists() { return AccessController.doPrivileged(new PrivilegedAction<Boolean>() { // Reason: Should be allowed. Obtaining an instance of ManagedFileImpl // requires privileges. public Boolean run() { return nativeFile.exists(); } }); } /* * (non-Javadoc) * * @see org.xamjwg.io.ManagedFile#getInputStream() */ public InputStream openInputStream() throws IOException { return RestrictedStore.this.getInputStream(this.nativeFile, this.path); } /* * (non-Javadoc) * * @see org.xamjwg.io.ManagedFile#getOutputStream() */ public OutputStream openOutputStream() throws IOException { return RestrictedStore.this.getOutputStream(this.nativeFile, this.path); } /* * (non-Javadoc) * * @see org.xamjwg.io.ManagedFile#getParent() */ public ManagedFile getParent() throws IOException { try { return AccessController.doPrivileged(new PrivilegedAction<ManagedFile>() { // Reason: Should be allowed. Obtaining an instance of // ManagedFileImpl // requires privileges. public ManagedFile run() { try { final File parentFile = nativeFile.getParentFile(); // Note: nativeToManaged checks canonical path for // permissions. return nativeToManaged(parentFile); } catch (final IOException ioe) { throw new WrapperException(ioe); } } }); } catch (final WrapperException we) { throw (IOException) we.getCause(); } } /* * (non-Javadoc) * * @see org.xamjwg.io.ManagedFile#getPath() */ public String getPath() { return this.path; } /* * (non-Javadoc) * * @see org.xamjwg.io.ManagedFile#isDirectory() */ public boolean isDirectory() { return AccessController.doPrivileged(new PrivilegedAction<Boolean>() { // Reason: Should be allowed. Obtaining an instance of ManagedFileImpl // requires privileges. public Boolean run() { return nativeFile.isDirectory(); } }); } /* * (non-Javadoc) * * @see org.xamjwg.io.ManagedFile#listFiles() */ public ManagedFile[] listFiles() throws IOException { return this.listFiles(null); } /* * (non-Javadoc) * * @see org.xamjwg.io.ManagedFile#listFiles(org.xamjwg.io.ManagedFileFilter) */ public ManagedFile[] listFiles(final ManagedFileFilter filter) throws IOException { try { return AccessController.doPrivileged(new PrivilegedAction<ManagedFile[]>() { // Reason: Should be allowed. Obtaining an instance of // ManagedFileImpl // requires privileges. public ManagedFile[] run() { try { final File[] files = nativeFile.listFiles(); final List<ManagedFile> mfs = new ArrayList<>(); for (final File file : files) { final ManagedFile mf = nativeToManaged(file); if ((filter == null) || filter.accept(mf)) { mfs.add(mf); } } return mfs.toArray(new ManagedFile[0]); } catch (final IOException ioe) { throw new WrapperException(ioe); } } }); } catch (final WrapperException we) { throw (IOException) we.getCause(); } } /* * (non-Javadoc) * * @see org.xamjwg.io.ManagedFile#mkdir() */ public boolean mkdir() { return AccessController.doPrivileged(new PrivilegedAction<Boolean>() { // Reason: Should be allowed. Obtaining an instance of ManagedFileImpl // requires privileges. public Boolean run() { final boolean success = nativeFile.mkdir(); if (success) { try { RestrictedStore.this.addUsedBytes(DIRECTORY_SIZE); } catch (final IOException ioe) { // Ignore } } return success; } }); } /* * (non-Javadoc) * * @see org.xamjwg.io.ManagedFile#mkdirs() */ public boolean mkdirs() { return AccessController.doPrivileged(new PrivilegedAction<Boolean>() { // Reason: Should be allowed. Obtaining an instance of ManagedFileImpl // requires privileges. public Boolean run() { final boolean success = nativeFile.mkdirs(); if (success) { try { RestrictedStore.this.addUsedBytes(DIRECTORY_SIZE); } catch (final IOException ioe) { // Ignore } } return success; } }); } /* * (non-Javadoc) * * @see org.xamjwg.io.ManagedFile#delete() */ public boolean delete() throws IOException { try { return AccessController.doPrivileged(new PrivilegedAction<Boolean>() { // Reason: Should be allowed. Obtaining an instance of ManagedFileImpl // requires privileges. public Boolean run() { try { final long prevLength = nativeFile.length() + EMPTY_FILE_SIZE; if (nativeFile.delete()) { subtractUsedBytes(prevLength); return true; } else { return false; } } catch (final IOException ioe) { throw new WrapperException(ioe); } } }); } catch (final WrapperException we) { throw (IOException) we.getCause(); } } } }