View Javadoc
1   /*
2    * Copyright 2018 The Apache Software Foundation.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.apache.river.osgi;
17  
18  import aQute.bnd.annotation.headers.ProvideCapability;
19  import aQute.bnd.annotation.headers.RequireCapability;
20  import java.io.ByteArrayInputStream;
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.lang.reflect.InvocationHandler;
25  import java.lang.reflect.Proxy;
26  import java.net.JarURLConnection;
27  import java.net.MalformedURLException;
28  import java.net.URISyntaxException;
29  import java.net.URL;
30  import java.security.cert.CertPath;
31  import java.security.cert.Certificate;
32  import java.security.cert.CertificateException;
33  import java.security.cert.CertificateFactory;
34  import java.util.Arrays;
35  import java.util.Collection;
36  import java.util.HashSet;
37  import java.util.Iterator;
38  import java.util.StringTokenizer;
39  import java.util.concurrent.ConcurrentHashMap;
40  import java.util.concurrent.ConcurrentMap;
41  import net.jini.core.constraint.MethodConstraints;
42  import net.jini.core.constraint.RemoteMethodControl;
43  import net.jini.export.CodebaseAccessor;
44  import net.jini.io.MarshalledInstance;
45  import net.jini.io.context.IntegrityEnforcement;
46  import net.jini.loader.ProxyCodebaseSpi;
47  import net.jini.security.Security;
48  import org.apache.river.api.net.Uri;
49  import org.apache.river.concurrent.RC;
50  import org.apache.river.concurrent.Ref;
51  import org.apache.river.concurrent.Referrer;
52  import org.osgi.framework.Bundle;
53  import org.osgi.framework.BundleActivator;
54  import org.osgi.framework.BundleContext;
55  import org.osgi.framework.BundleException;
56  
57  /**
58   *
59   * @author peter
60   */
61  @RequireCapability(
62  	ns="osgi.extender",
63  	filter="(osgi.extender=osgi.serviceloader.registrar)")
64  @ProvideCapability(
65  	ns="osgi.serviceloader",
66  	name="net.jini.loader.ProxyCodebaseSpi")
67  public class ProxyBundleProvider implements ProxyCodebaseSpi, BundleActivator {
68      private static final ConcurrentMap<String,Uri[]> uriCache;
69      private static final ConcurrentMap<Key,ClassLoader> cache;
70      
71      static {
72  	cache = new ConcurrentHashMap<Key,ClassLoader>();
73  	ConcurrentMap<Referrer<String>,Referrer<Uri[]>> intern1 =
74                  new ConcurrentHashMap<Referrer<String>,Referrer<Uri[]>>();
75          uriCache = RC.concurrentMap(intern1, Ref.TIME, Ref.STRONG, 60000L, 60000L);
76      }
77      
78      volatile BundleContext bc;
79      
80      
81      public ProxyBundleProvider(){
82  	this.bc = null;
83      }
84      
85      /**
86       * Determines if the URL is pointing to a directory.
87       */
88      private static boolean isDirectory(URL url) {
89          String file = url.getFile();
90          return (file.length() > 0 && file.charAt(file.length() - 1) == File.separatorChar);
91      }
92      
93      /**
94       * Convert a string containing a space-separated list of URL Strings into a
95       * corresponding array of Uri objects, throwing a MalformedURLException
96       * if any of the URLs are invalid.  This method returns null if the
97       * specified string is null.
98       *
99       * @param path the string path to be converted to an array of urls
100      * @return the string path converted to an array of URLs, or null
101      * @throws MalformedURLException if the string path of urls contains a
102      *         mal-formed url which can not be converted into a url object.
103      */
104     private Uri[] pathToURIs(String path) throws MalformedURLException {
105 	if (path == null) {
106 	    return null;
107 	}
108         Uri[] urls = uriCache.get(path); // Cache of previously converted strings.
109         if (urls != null) return urls;
110 	StringTokenizer st = new StringTokenizer(path);	// divide by spaces
111 	urls = new Uri[st.countTokens()];
112 	for (int i = 0; st.hasMoreTokens(); i++) {
113             try {
114                 String ur = st.nextToken();
115                 ur = Uri.fixWindowsURI(ur);
116                 urls[i] = Uri.parseAndCreate(ur);
117             } catch (URISyntaxException ex) {
118                 throw new MalformedURLException("URL's must be RFC 3986 Compliant: " 
119                         + ex.getMessage());
120             }
121 	}
122         Uri [] existed = uriCache.putIfAbsent(path, urls);
123         if (existed != null) urls = existed;
124 	return urls;
125     }
126 
127     @Override
128     public Object resolve(CodebaseAccessor bootstrapProxy,
129 			MarshalledInstance smartProxy,
130 			ClassLoader parent,
131 			ClassLoader verifier,
132 			Collection context) 
133 	    throws IOException, ClassNotFoundException
134     {
135 	if (bc == null) throw new NullPointerException("Bundle is not active");
136 	if (context == null) throw new NullPointerException(
137 		"stream context cannot be null");
138 	if (!(bootstrapProxy instanceof RemoteMethodControl)) 
139 	    throw new IllegalArgumentException(
140 		    "bootstrap proxy must be instance of RemoteMethodControl");
141 	Iterator it = context.iterator();
142 	MethodConstraints mc = null;
143 	IntegrityEnforcement integrityEnforcement = null;
144 	while(it.hasNext()){
145 	    Object o = it.next();
146 	    if (o instanceof MethodConstraints){
147 		mc = (MethodConstraints) o;
148 	    } else if (o instanceof IntegrityEnforcement){
149 		integrityEnforcement = (IntegrityEnforcement) o;
150 	    }
151 	}
152 	bootstrapProxy = (CodebaseAccessor) 
153 		((RemoteMethodControl) bootstrapProxy).setConstraints(mc); // MinPrincipal happens here.
154 	String path = bootstrapProxy.getClassAnnotation();
155 	String proxyBundlePath = null;
156 	Uri [] codebases = pathToURIs(path);
157 	Key loaderKey = new Key(Proxy.getInvocationHandler(bootstrapProxy), codebases[0]);
158 	ClassLoader loader = cache.get(loaderKey);
159 	if (loader == null){
160 	    byte [] encodedCerts = bootstrapProxy.getEncodedCerts();
161 	    Collection<? extends Certificate> certs = null;
162 	    if ((encodedCerts == null 
163 		|| encodedCerts.length == 0 )
164 		&& integrityEnforcement != null
165 		&& integrityEnforcement.integrityEnforced())
166 	    {
167 		Security.verifyCodebaseIntegrity(path, null);
168 	    } else if (encodedCerts != null && encodedCerts.length > 0) {
169 		// Although we trust the bootstrapProxy now, if we require validation,
170 		// we must check the jar file has been signed.
171 		try {
172 		    String certFactoryType = bootstrapProxy.getCertFactoryType();
173 		    String certPathEncoding = bootstrapProxy.getCertPathEncoding();
174 		    CertificateFactory factory =
175 			    CertificateFactory.getInstance(certFactoryType);
176 		    CertPath certPath = factory.generateCertPath(
177 			    new ByteArrayInputStream(encodedCerts), certPathEncoding);
178 		    certs = certPath.getCertificates();
179 		    proxyBundlePath = codebases[0].toString();
180 		    URL searchURL = createSearchURL(new URL(proxyBundlePath));
181 		    URL jarURL = ((JarURLConnection) searchURL
182 			.openConnection()).getJarFileURL();
183 		    JarURLConnection juc = (JarURLConnection) new URL(
184 			    "jar", "", //$NON-NLS-1$ //$NON-NLS-2$
185 			    jarURL.toExternalForm() + "!/").openConnection(); //$NON-NLS-1$
186 		    juc.connect();
187 		    InputStream in = juc.getInputStream();
188 		    byte [] bytes = new byte[1024];
189 		    int bytesRead = 0;
190 		    // reading in the entire jar file will check it's validity.
191 		    // it will also be cached.
192 		    do { // keep reading until we reach end of stream.
193 			bytesRead = in.read(bytes);
194 		    } while (bytesRead == 1024);
195 		    // We should be able to read certs now, confirming the jar 
196 		    // has been verified.
197 		    Certificate [] certificates = juc.getCertificates();
198 		    if (certs == null){
199 			throw new SecurityException("jar file invalid");
200 		    }
201 		    // Check our certs match.
202 		    HashSet<Certificate> actualCerts 
203 			    = new HashSet<Certificate>(Arrays.asList(certificates));
204 		    HashSet<Certificate> requiredCerts = new HashSet<Certificate>(certs);
205 		    if (!actualCerts.containsAll(requiredCerts)){
206 			throw new SecurityException("certificates don't match");
207 		    }
208 		} catch (CertificateException ex) {
209 		    throw new IOException("Problem creating signer certificates", ex);
210 		} 
211 	    }
212 	    if (proxyBundlePath == null) proxyBundlePath = codebases[0].toString();
213 	    try {
214 		Bundle proxyBundle = bc.installBundle(proxyBundlePath);
215 		loader = new BundleDelegatingClassLoader(proxyBundle);
216 		ClassLoader existed = cache.putIfAbsent(loaderKey, loader);
217 		if (existed != null){
218 		    loader = existed;
219 		    try {
220 			proxyBundle.uninstall();
221 		    } catch (BundleException ex){}// Ignore
222 		}
223 	    } catch (BundleException ex) {
224 		throw new IOException("Unable to resolve Bundle", ex);
225 	    }
226 	}
227 	Object sp = smartProxy.get(loader, true, null, context);
228 //	if (sp instanceof RemoteMethodControl)
229 //	    sp = ((RemoteMethodControl)sp).setConstraints(mc);
230 	return sp;
231     }
232 
233     public void start(BundleContext bc) throws Exception {
234 	this.bc = bc;
235     }
236 
237     public void stop(BundleContext bc) throws Exception {
238 	this.bc = null;
239     }
240     
241     /**
242      * Returns an URL that will be checked if it contains the class or resource.
243      * If the file component of the URL is not a directory, a Jar URL will be
244      * created.
245      *
246      * @return java.net.URL a test URL
247      */
248     private URL createSearchURL(URL url) throws MalformedURLException {
249         if (url == null) return url;
250         String protocol = url.getProtocol();
251         if (isDirectory(url) || protocol.equals("jar")) { //$NON-NLS-1$
252             return url;
253         }
254 	return new URL("jar", "", //$NON-NLS-1$ //$NON-NLS-2$
255 		-1, url.toString() + "!/"); //$NON-NLS-1$
256     }
257 
258     public boolean substitute(Class serviceClass, ClassLoader streamLoader) {
259 	// Simply checks whether the class is visible at this endpoint,
260 	// if it is, it should also be at the remote endpoint
261 	// so shouldn't need substitution as OSGi
262 	// environments are expected to have identical bundles at
263 	// each endpoint.
264 	String name = serviceClass.getName();
265 	try {
266 	    Class found = streamLoader.loadClass(name);
267 	    return !found.equals(serviceClass);
268 	} catch (ClassNotFoundException e){
269 	    return true;
270 	}
271     }
272     
273     private static class Key {
274 	private final InvocationHandler handler;
275 	private final Uri codebase;
276 	private final int hashcode;
277 	
278 	Key(InvocationHandler h, Uri codebase){
279 	    this.handler = h;
280 	    this.codebase = codebase;
281 	    int hash = 5;
282 	    hash = 73 * hash + (this.handler != null ? this.handler.hashCode() : 0);
283 	    hash = 73 * hash + (this.codebase != null ? this.codebase.hashCode() : 0);
284 	    this.hashcode = hash;
285 	}
286 
287 	@Override
288 	public int hashCode() {
289 	    return hashcode;
290 	}
291 	
292 	@Override
293 	public boolean equals(Object o){
294 	    if (!(o instanceof Key)) return false;
295 	    if (!handler.equals(((Key)o).handler)) return false;
296 	    return codebase.equals(((Key)o).codebase);
297 	}
298     }
299     
300 }