AbstractArchiveResource.java
/*
* 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.catalina.webresources;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.cert.Certificate;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.jar.JarEntry;
import java.util.jar.Manifest;
import org.apache.catalina.util.URLEncoder;
public abstract class AbstractArchiveResource extends AbstractResource {
private final AbstractArchiveResourceSet archiveResourceSet;
private final String baseUrl;
private final JarEntry resource;
private final String codeBaseUrl;
private final String name;
private boolean readCerts = false;
private Certificate[] certificates;
/*
* Deprecated even though this is the "new" constructor as code needs to call the old constructor for now.
*/
@Deprecated
protected AbstractArchiveResource(AbstractArchiveResourceSet archiveResourceSet, String webAppPath, String baseUrl,
JarEntry jarEntry) {
this(archiveResourceSet, webAppPath, baseUrl, jarEntry, null);
}
/*
* The expectation is that this will be deprecated and then removed once the SecurityManager has been fully removed
* from the JRE and it has been confirmed that the JRE no longer depends on code base.
*
* See https://bz.apache.org/bugzilla/show_bug.cgi?id=69426
*/
protected AbstractArchiveResource(AbstractArchiveResourceSet archiveResourceSet, String webAppPath, String baseUrl,
JarEntry jarEntry, String codeBaseUrl) {
super(archiveResourceSet.getRoot(), webAppPath);
this.archiveResourceSet = archiveResourceSet;
this.baseUrl = baseUrl;
this.resource = jarEntry;
this.codeBaseUrl = codeBaseUrl;
String resourceName = resource.getName();
if (resourceName.charAt(resourceName.length() - 1) == '/') {
resourceName = resourceName.substring(0, resourceName.length() - 1);
}
String internalPath = archiveResourceSet.getInternalPath();
if (internalPath.length() > 0 && resourceName.equals(internalPath.subSequence(1, internalPath.length()))) {
name = "";
} else {
int index = resourceName.lastIndexOf('/');
if (index == -1) {
name = resourceName;
} else {
name = resourceName.substring(index + 1);
}
}
}
protected AbstractArchiveResourceSet getArchiveResourceSet() {
return archiveResourceSet;
}
protected final String getBase() {
return archiveResourceSet.getBase();
}
protected final String getBaseUrl() {
return baseUrl;
}
protected final JarEntry getResource() {
return resource;
}
@Override
public long getLastModified() {
return resource.getTime();
}
@Override
public boolean exists() {
return true;
}
@Override
public boolean isVirtual() {
return false;
}
@Override
public boolean isDirectory() {
return resource.isDirectory();
}
@Override
public boolean isFile() {
return !resource.isDirectory();
}
@Override
public boolean delete() {
return false;
}
@Override
public String getName() {
return name;
}
@Override
public long getContentLength() {
if (isDirectory()) {
return -1;
}
return resource.getSize();
}
@Override
public String getCanonicalPath() {
return null;
}
@Override
public boolean canRead() {
return true;
}
@Override
public long getCreation() {
return resource.getTime();
}
@Override
public URL getURL() {
String url = baseUrl + URLEncoder.DEFAULT.encode(resource.getName(), StandardCharsets.UTF_8);
try {
return new URI(url).toURL();
} catch (MalformedURLException | URISyntaxException | IllegalArgumentException e) {
if (getLog().isDebugEnabled()) {
getLog().debug(sm.getString("fileResource.getUrlFail", url), e);
}
return null;
}
}
@Override
public URL getCodeBase() {
try {
return new URI(codeBaseUrl).toURL();
} catch (MalformedURLException | URISyntaxException e) {
if (getLog().isDebugEnabled()) {
getLog().debug(sm.getString("fileResource.getUrlFail", codeBaseUrl), e);
}
return null;
}
}
@Override
public final byte[] getContent() {
long len = getContentLength();
if (len > Integer.MAX_VALUE) {
// Can't create an array that big
throw new ArrayIndexOutOfBoundsException(
sm.getString("abstractResource.getContentTooLarge", getWebappPath(), Long.valueOf(len)));
}
if (len < 0) {
// Content is not applicable here (e.g. is a directory)
return null;
}
int size = (int) len;
byte[] result = new byte[size];
int pos = 0;
try (JarInputStreamWrapper jisw = getJarInputStreamWrapper()) {
if (jisw == null) {
// An error occurred, don't return corrupted content
return null;
}
while (pos < size) {
int n = jisw.read(result, pos, size - pos);
if (n < 0) {
break;
}
pos += n;
}
// Once the stream has been read, read the certs
certificates = jisw.getCertificates();
readCerts = true;
} catch (IOException ioe) {
if (getLog().isDebugEnabled()) {
getLog().debug(sm.getString("abstractResource.getContentFail", getWebappPath()), ioe);
}
// Don't return corrupted content
return null;
}
return result;
}
@Override
public Certificate[] getCertificates() {
if (!readCerts) {
// TODO - get content first
throw new IllegalStateException();
}
return certificates;
}
@Override
public Manifest getManifest() {
return archiveResourceSet.getManifest();
}
@Override
protected final InputStream doGetInputStream() {
if (isDirectory()) {
return null;
}
return getJarInputStreamWrapper();
}
protected abstract JarInputStreamWrapper getJarInputStreamWrapper();
/**
* This wrapper assumes that the InputStream was created from a JarFile obtained from a call to
* getArchiveResourceSet().openJarFile(). If this is not the case then the usage counting in
* AbstractArchiveResourceSet will break and the JarFile may be unexpectedly closed.
*/
protected class JarInputStreamWrapper extends InputStream {
private final JarEntry jarEntry;
private final InputStream is;
private final AtomicBoolean closed = new AtomicBoolean(false);
public JarInputStreamWrapper(JarEntry jarEntry, InputStream is) {
this.jarEntry = jarEntry;
this.is = is;
}
@Override
public int read() throws IOException {
return is.read();
}
@Override
public int read(byte[] b) throws IOException {
return is.read(b);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return is.read(b, off, len);
}
@Override
public long skip(long n) throws IOException {
return is.skip(n);
}
@Override
public int available() throws IOException {
return is.available();
}
@Override
public void close() throws IOException {
if (closed.compareAndSet(false, true)) {
// Must only call this once else the usage counting will break
archiveResourceSet.closeJarFile();
}
is.close();
}
@Override
public synchronized void mark(int readlimit) {
is.mark(readlimit);
}
@Override
public synchronized void reset() throws IOException {
is.reset();
}
@Override
public boolean markSupported() {
return is.markSupported();
}
public Certificate[] getCertificates() {
return jarEntry.getCertificates();
}
}
}