/* * Password Management Servlets (PWM) * http://www.pwm-project.org * * Copyright (c) 2006-2009 Novell, Inc. * Copyright (c) 2009-2017 The PWM 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 version 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 program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package password.pwm.http.servlet.resource; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import org.apache.commons.io.output.NullOutputStream; import password.pwm.AppProperty; import password.pwm.PwmApplication; import password.pwm.PwmConstants; import password.pwm.error.PwmException; import password.pwm.error.PwmUnrecoverableException; import password.pwm.health.HealthRecord; import password.pwm.http.PwmRequest; import password.pwm.svc.PwmService; import password.pwm.svc.stats.EventRateMeter; import password.pwm.util.java.FileSystemUtility; import password.pwm.util.java.JavaHelper; import password.pwm.util.java.Percent; import password.pwm.util.java.TimeDuration; import password.pwm.util.logging.PwmLogger; import password.pwm.util.secure.ChecksumOutputStream; import password.pwm.util.secure.PwmHashAlgorithm; import javax.servlet.ServletContext; import java.io.File; import java.io.IOException; import java.math.BigDecimal; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; public class ResourceServletService implements PwmService { private static final PwmLogger LOGGER = PwmLogger.forClass(ResourceServletService.class); private ResourceServletConfiguration resourceServletConfiguration; private Cache<CacheKey, CacheEntry> cache; private EventRateMeter.MovingAverage cacheHitRatio = new EventRateMeter.MovingAverage(60 * 60 * 1000); private String resourceNonce; private STATUS status = STATUS.NEW; private PwmApplication pwmApplication; public String getResourceNonce() { return resourceNonce; } public Cache<CacheKey, CacheEntry> getCacheMap() { return cache; } public EventRateMeter.MovingAverage getCacheHitRatio() { return cacheHitRatio; } public long bytesInCache() { final Map<CacheKey, CacheEntry> cacheCopy = new HashMap<>(cache.asMap()); long cacheByteCount = 0; for (final CacheKey cacheKey : cacheCopy.keySet()) { final CacheEntry cacheEntry = cacheCopy.get(cacheKey); if (cacheEntry != null && cacheEntry.getEntity() != null) { cacheByteCount += cacheEntry.getEntity().length; } } return cacheByteCount; } public int itemsInCache() { final Cache<CacheKey, CacheEntry> responseCache = getCacheMap(); return (int)responseCache.estimatedSize(); } public Percent cacheHitRatio() { final BigDecimal numerator = BigDecimal.valueOf(getCacheHitRatio().getAverage()); final BigDecimal denominator = BigDecimal.ONE; return new Percent(numerator, denominator); } @Override public STATUS status() { return status; } @Override public void init(final PwmApplication pwmApplication) throws PwmException { this.pwmApplication = pwmApplication; status = STATUS.OPENING; try { this.resourceServletConfiguration = ResourceServletConfiguration.createResourceServletConfiguration(pwmApplication); cache = Caffeine.newBuilder() .maximumSize(resourceServletConfiguration.getMaxCacheItems()) .build(); status = STATUS.OPEN; } catch (Exception e) { LOGGER.error("error during cache initialization, will remain closed; error: " + e.getMessage()); status = STATUS.CLOSED; return; } try { resourceNonce = makeResourcePathNonce(); } catch (Exception e) { LOGGER.error("error during nonce generation, will remain closed; error: " + e.getMessage()); status = STATUS.CLOSED; } } @Override public void close() { } @Override public List<HealthRecord> healthCheck() { return Collections.emptyList(); } @Override public ServiceInfo serviceInfo() { return null; } ResourceServletConfiguration getResourceServletConfiguration() { return resourceServletConfiguration; } private String makeResourcePathNonce() throws PwmUnrecoverableException, IOException { final boolean enablePathNonce = Boolean.parseBoolean(pwmApplication.getConfig().readAppProperty(AppProperty.HTTP_RESOURCES_ENABLE_PATH_NONCE)); if (!enablePathNonce) { return ""; } final Instant startTime = Instant.now(); final ChecksumOutputStream checksumOs = new ChecksumOutputStream(PwmHashAlgorithm.MD5, new NullOutputStream()); if (pwmApplication.getPwmEnvironment().getContextManager() != null) { try { final File webInfPath = pwmApplication.getPwmEnvironment().getContextManager().locateWebInfFilePath(); if (webInfPath != null && webInfPath.exists()) { final File basePath = webInfPath.getParentFile(); if (basePath != null && basePath.exists()) { final File resourcePath = new File(basePath.getAbsolutePath() + File.separator + "public" + File.separator + "resources"); if (resourcePath.exists()) { final List<FileSystemUtility.FileSummaryInformation> fileSummaryInformations = new ArrayList<>(); fileSummaryInformations.addAll(FileSystemUtility.readFileInformation(resourcePath)); for (final FileSystemUtility.FileSummaryInformation fileSummaryInformation : fileSummaryInformations) { checksumOs.write((fileSummaryInformation.getSha1sum()).getBytes(PwmConstants.DEFAULT_CHARSET)); } } } } } catch (Exception e) { LOGGER.error("unable to generate resource path nonce: " + e.getMessage()); } } for (final FileResource fileResource : getResourceServletConfiguration().getCustomFileBundle().values()) { JavaHelper.copyWhilePredicate(fileResource.getInputStream(), checksumOs, o -> true); } if (getResourceServletConfiguration().getZipResources() != null) { for (final String key : getResourceServletConfiguration().getZipResources().keySet()) { final ZipFile value = getResourceServletConfiguration().getZipResources().get(key); checksumOs.write(key.getBytes(PwmConstants.DEFAULT_CHARSET)); for (Enumeration<? extends ZipEntry> zipEnum = value.entries(); zipEnum.hasMoreElements(); ) { final ZipEntry entry = zipEnum.nextElement(); checksumOs.write(Long.toHexString(entry.getSize()).getBytes(PwmConstants.DEFAULT_CHARSET)); } } } final String nonce = JavaHelper.byteArrayToHexString(checksumOs.getInProgressChecksum()).toLowerCase(); LOGGER.debug("completed generation of nonce '" + nonce + "' in " + TimeDuration.fromCurrent(startTime).asCompactString()); final String noncePrefix = pwmApplication.getConfig().readAppProperty(AppProperty.HTTP_RESOURCES_NONCE_PATH_PREFIX); return "/" + noncePrefix + nonce; } public boolean checkIfThemeExists(final PwmRequest pwmRequest, final String themeName) throws PwmUnrecoverableException { if (themeName == null) { return false; } if (themeName.equals(ResourceFileServlet.EMBED_THEME)) { return true; } if (!themeName.matches(pwmRequest.getConfig().readAppProperty(AppProperty.SECURITY_INPUT_THEME_MATCH_REGEX))) { LOGGER.warn(pwmRequest, "discarding suspicious theme name in request: " + themeName); return false; } final ServletContext servletContext = pwmRequest.getHttpServletRequest().getServletContext(); final String[] testUrls = new String[]{ ResourceFileServlet.THEME_CSS_PATH, ResourceFileServlet.THEME_CSS_MOBILE_PATH }; for (final String testUrl : testUrls) { final String themePathUrl = ResourceFileServlet.RESOURCE_PATH + testUrl.replace(ResourceFileServlet.TOKEN_THEME, themeName); final FileResource resolvedFile = ResourceFileServlet.resolveRequestedFile(servletContext, themePathUrl, getResourceServletConfiguration()); if (resolvedFile != null && resolvedFile.exists()) { LOGGER.debug(pwmRequest, "check for theme validity of '" + themeName + "' returned true"); return true; } } LOGGER.debug(pwmRequest, "check for theme validity of '" + themeName + "' returned false"); return false; } }