001/**
002 *
003 * Copyright (c) 2014, the Railo Company Ltd. All rights reserved.
004 *
005 * This library is free software; you can redistribute it and/or
006 * modify it under the terms of the GNU Lesser General Public
007 * License as published by the Free Software Foundation; either 
008 * version 2.1 of the License, or (at your option) any later version.
009 * 
010 * This library is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013 * Lesser General Public License for more details.
014 * 
015 * You should have received a copy of the GNU Lesser General Public 
016 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
017 * 
018 **/
019package lucee.commons.lang;
020
021import java.io.ByteArrayInputStream;
022import java.io.Closeable;
023import java.io.FileNotFoundException;
024import java.io.IOException;
025import java.io.InputStream;
026import java.net.URL;
027import java.security.PublicKey;
028import java.util.zip.ZipEntry;
029import java.util.zip.ZipFile;
030
031import lucee.commons.digest.RSA;
032import lucee.commons.io.IOUtil;
033import lucee.commons.io.res.Resource;
034import lucee.commons.io.res.util.FileWrapper;
035import lucee.runtime.type.Sizeable;
036import lucee.transformer.util.AlreadyClassException;
037
038// TODO umbauen auf ZipInputStream oder ein wrapper schreiben fuer resorces der das file interface einhaelt
039
040
041public final class ArchiveClassLoader extends ClassLoader implements Sizeable,Closeable {
042    
043    private final ZipFile zip;
044    private final ClassLoader pcl;
045    
046    /**
047     * constructor of the class
048     * @param file
049     * @param parent
050     * @throws IOException
051     */
052    public ArchiveClassLoader(Resource file, ClassLoader parent) throws IOException {
053        super(parent);
054        this.pcl=parent;
055        //this.file=file;
056        
057        //print.ln("archive:"+file.getPath());
058        if(!file.exists())
059            throw new FileNotFoundException("file "+file.getAbsolutePath()+" doesn't exist");
060        if(!file.isFile())
061            throw new IOException(file.getAbsolutePath()+" is not a file");
062        if(!file.isReadable())
063            throw new IOException("no access to "+file.getAbsolutePath()+" file");
064        
065        
066        this.zip=new ZipFile(FileWrapper.toFile(file));
067    }
068    
069    /**
070     * Loads the class with the specified name. This method searches for 
071     * classes in the same manner as the {@link #loadClass(String, boolean)} 
072     * method. It is called by the Java virtual machine to resolve class 
073     * references. Calling this method is equivalent to calling 
074     * <code>loadClass(name, false)</code>.
075     *
076     * @param     name the name of the class
077     * @return    the resulting <code>Class</code> object
078     * @exception ClassNotFoundException if the class was not found
079     */
080   public Class loadClass(String name) throws ClassNotFoundException   {
081       return loadClass(name, false);
082   }
083
084    /**
085     * Loads the class with the specified name.  The default implementation of
086     * this method searches for classes in the following order:<p>
087     *
088     * <ol>
089     * <li> Call {@link #findLoadedClass(String)} to check if the class has
090     *      already been loaded. <p>
091     * <li> Call the <code>loadClass</code> method on the parent class
092     *      loader.  If the parent is <code>null</code> the class loader
093     *      built-in to the virtual machine is used, instead. <p>
094     * <li> Call the {@link #findClass(String)} method to find the class. <p>
095     * </ol>
096     *
097     * If the class was found using the above steps, and the
098     * <code>resolve</code> flag is true, this method will then call the
099     * {@link #resolveClass(Class)} method on the resulting class object.
100     * <p>
101     * From the Java 2 SDK, v1.2, subclasses of ClassLoader are 
102     * encouraged to override
103     * {@link #findClass(String)}, rather than this method.<p>
104     *
105     * @param     name the name of the class
106     * @param     resolve if <code>true</code> then resolve the class
107     * @return   the resulting <code>Class</code> object
108     * @exception ClassNotFoundException if the class could not be found
109     */
110    protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
111        // First, check if the class has already been loaded
112        Class c = findLoadedClass(name);
113        if (c == null) {
114            c=findClassEL(name);
115            if(c==null) {
116                c =pcl.loadClass(name);
117            }
118        }
119        if (resolve) {
120            resolveClass(c);
121        }
122        return c;
123   }
124    
125    @Override
126    protected Class findClass(String name) throws ClassNotFoundException {
127        Class clazz=findClassEL(name);
128        if(clazz!=null) return clazz;
129        throw new ClassNotFoundException("class "+name+" not found");
130    }
131    
132    private Class findClassEL(String name) {
133        byte[] barr = getBytes(name.replace('.','/').concat(".class"));
134        if(barr!=null) {
135            try {
136                barr = toRaw(barr);
137                return defineClass(name,barr,0,barr.length);
138            }
139            catch(Throwable t) {
140                ExceptionUtil.rethrowIfNecessary(t);
141            }
142        }
143        return null;
144    }
145
146    @Override
147    public InputStream getResourceAsStream(String name) {
148        InputStream is = super.getResourceAsStream(name);
149        if(is!=null) return is;
150        
151        byte[] barr = toRaw(getBytes(name));
152        if(barr!=null) return new ByteArrayInputStream(barr);
153        
154        return null;
155    }
156    
157    
158    private byte[] toRaw(byte[] bytes) {
159        if(bytes==null || !ClassUtil.isEncryptedBytecode(bytes)) return bytes;
160                
161                String str = System.getenv("PUBLIC_KEY");
162                if(StringUtil.isEmpty(str,true)) str=System.getProperty("PUBLIC_KEY");
163                if(StringUtil.isEmpty(str,true)) throw new RuntimeException("to decrypt encrypted bytecode, you need to set PUBLIC_KEY as system property or or enviroment variable");
164                
165                try {   
166                        PublicKey publicKey = RSA.toPublicKey(str);
167                        // first 2 bytes are just a mask to detect encrypted code, so we need to set offset 2
168                        bytes=RSA.decrypt(bytes, publicKey,2);
169                }
170                catch (Exception e) {
171                        throw new RuntimeException(e);
172                }
173                return bytes;
174        }
175    
176    
177    @Override 
178    public URL getResource(String name) { 
179        return null; 
180    } 
181
182
183    private byte[] getBytes(String name) {
184     
185        ZipEntry entry= zip.getEntry(name);
186        
187        if (entry == null)return null;
188        
189        int size= (int) entry.getSize();
190        InputStream is=null;
191        try {
192            is= zip.getInputStream(entry);
193            byte[] data= new byte[size];
194            int pos= 0;
195            while (pos < size) {
196                int n= is.read(data, pos, data.length - pos);
197                pos += n;
198            }
199            return data;
200        }
201        catch(IOException ioe) {}
202        finally {
203            IOUtil.closeEL(is);
204        }
205        return null;
206    }
207
208        @Override
209        public long sizeOf() {
210                return SizeOf.size(zip);
211        }
212
213        public void close() throws IOException {
214                zip.close();
215        } 
216}