/* * Copyright 2014 Google Inc. * * 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 com.google.gwt.dev.url; import com.google.gwt.thirdparty.guava.common.collect.HashMultimap; import com.google.gwt.thirdparty.guava.common.collect.Multimap; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.JarURLConnection; import java.net.URL; import java.net.URLConnection; import java.security.Permission; import java.util.Collection; import java.util.List; import java.util.Map; /** * Opens connections to Zip files when requested and gathers them in a list for later closing. * <p> * This closing functionality makes it possible to run untrusted plugin code (for example * Generators) and still guarantee that there will be no accidental attempts to read from old and * deleted Zip files using stale InputStreams. */ @SuppressWarnings("restriction") public class CloseableJarHandler extends sun.net.www.protocol.jar.Handler { /** * Passes through all URLConnection access but collects all created InputStreams. */ private class CloseableUrlConnection extends URLConnection { private JarURLConnection jarUrlConnection; public CloseableUrlConnection(URL jarUrl, JarURLConnection jarUrlConnection) { super(jarUrl); this.jarUrlConnection = jarUrlConnection; } @Override public void addRequestProperty(String key, String value) { jarUrlConnection.addRequestProperty(key, value); } @Override public void connect() throws IOException { jarUrlConnection.connect(); } @Override public boolean getAllowUserInteraction() { return jarUrlConnection.getAllowUserInteraction(); } @Override public int getConnectTimeout() { return jarUrlConnection.getConnectTimeout(); } @Override public Object getContent() throws IOException { return jarUrlConnection.getContent(); } @Override public Object getContent(Class[] classes) throws IOException { return jarUrlConnection.getContent(classes); } @Override public String getContentEncoding() { return jarUrlConnection.getContentEncoding(); } @Override public int getContentLength() { return jarUrlConnection.getContentLength(); } // No @Override as the method is only available in Java 7. public long getContentLengthLong() { int contentLength = jarUrlConnection.getContentLength(); if (contentLength == -1) { throw new RuntimeException("Content length could not be read because it exceeded " + Integer.MAX_VALUE + " bytes."); } return contentLength; } @Override public String getContentType() { return jarUrlConnection.getContentType(); } @Override public long getDate() { return jarUrlConnection.getDate(); } @Override public boolean getDefaultUseCaches() { return jarUrlConnection.getDefaultUseCaches(); } @Override public boolean getDoInput() { return jarUrlConnection.getDoInput(); } @Override public boolean getDoOutput() { return jarUrlConnection.getDoOutput(); } @Override public long getExpiration() { return jarUrlConnection.getExpiration(); } @Override public String getHeaderField(int n) { return jarUrlConnection.getHeaderField(n); } // No need to override getHeaderField<Blah> as they all get routed to get getHeaderField() @Override public String getHeaderField(String name) { return jarUrlConnection.getHeaderField(name); } @Override public String getHeaderFieldKey(int n) { return jarUrlConnection.getHeaderFieldKey(n); } @Override public Map<String, List<String>> getHeaderFields() { return jarUrlConnection.getHeaderFields(); } @Override public long getIfModifiedSince() { return jarUrlConnection.getIfModifiedSince(); } @Override public InputStream getInputStream() throws IOException { InputStream inputStream = jarUrlConnection.getInputStream(); URL jarFileURL = jarUrlConnection.getJarFileURL(); inputStreamsByJarFilePath.put(jarFileURL.getFile(), inputStream); return inputStream; } @Override public long getLastModified() { return jarUrlConnection.getLastModified(); } @Override public OutputStream getOutputStream() throws IOException { return jarUrlConnection.getOutputStream(); } @Override public Permission getPermission() throws IOException { return jarUrlConnection.getPermission(); } @Override public int getReadTimeout() { return jarUrlConnection.getReadTimeout(); } @Override public Map<String, List<String>> getRequestProperties() { return jarUrlConnection.getRequestProperties(); } @Override public String getRequestProperty(String key) { return jarUrlConnection.getRequestProperty(key); } @Override public URL getURL() { return jarUrlConnection.getURL(); } @Override public boolean getUseCaches() { return jarUrlConnection.getUseCaches(); } @Override public void setAllowUserInteraction(boolean allowuserinteraction) { jarUrlConnection.setAllowUserInteraction(allowuserinteraction); } @Override public void setConnectTimeout(int timeout) { jarUrlConnection.setConnectTimeout(timeout); } @Override public void setDefaultUseCaches(boolean defaultusecaches) { jarUrlConnection.setDefaultUseCaches(defaultusecaches); } @Override public void setDoInput(boolean doinput) { jarUrlConnection.setDoInput(doinput); } @Override public void setDoOutput(boolean dooutput) { jarUrlConnection.setDoOutput(dooutput); } @Override public void setIfModifiedSince(long ifmodifiedsince) { jarUrlConnection.setIfModifiedSince(ifmodifiedsince); } @Override public void setReadTimeout(int timeout) { jarUrlConnection.setReadTimeout(timeout); } @Override public void setRequestProperty(String key, String value) { jarUrlConnection.setRequestProperty(key, value); } @Override public void setUseCaches(boolean usecaches) { jarUrlConnection.setUseCaches(usecaches); } @Override public String toString() { return jarUrlConnection.toString(); } } private Multimap<String, InputStream> inputStreamsByJarFilePath = HashMultimap.create(); /** * Closes all InputStreams that were created by the URL system that point at the given Jar file. */ public void closeStreams(String jarFilePath) throws IOException { Collection<InputStream> inputStreams = inputStreamsByJarFilePath.get(jarFilePath); if (inputStreams == null) { return; } for (InputStream inputStream : inputStreams) { inputStream.close(); } inputStreamsByJarFilePath.removeAll(jarFilePath); } @Override protected URLConnection openConnection(URL jarUrl) throws IOException { // Let the Jar system do the heavy lifting of opening the connection. JarURLConnection jarUrlConnection = (JarURLConnection) super.openConnection(jarUrl); // Ensures that when all connections have been closed the cached Zip file references will be // cleared. jarUrlConnection.setUseCaches(false); // Wrap the jar url connection in a way that will collect all created InputStreams so that they // can be closed. return new CloseableUrlConnection(jarUrl, jarUrlConnection); } }