/* * Copyright (c) 2013-2014 the original author or authors * * 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 io.werval.controllers; import java.util.List; import io.werval.api.Mode; import io.werval.api.outcomes.Outcome; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static io.werval.api.context.CurrentContext.application; import static io.werval.api.context.CurrentContext.mimeTypes; import static io.werval.api.context.CurrentContext.outcomes; import static io.werval.api.context.CurrentContext.request; import static io.werval.api.context.CurrentContext.response; import static io.werval.api.http.Headers.Names.CACHE_CONTROL; import static io.werval.api.http.Headers.Names.CONTENT_TYPE; import static io.werval.api.mime.MimeTypesNames.APPLICATION_OCTET_STREAM; import static io.werval.util.Charsets.US_ASCII; import static io.werval.util.ClassLoaders.resourceExists; import static io.werval.util.IllegalArguments.ensureNotEmpty; import static io.werval.util.Strings.withoutHead; import static io.werval.util.Strings.withoutTrail; import static java.util.Locale.US; /** * Serve resources from the classpath. * <p> * Always use chunked transfer encoding. * <p> * MimeType detection done using Application MimeTypes, fallback to <code>application/octet-stream</code>. * <p> * Log 404 at DEBUG level. * <p> * Log 200 at TRACE level. */ public class Classpath { private static final Logger LOG = LoggerFactory.getLogger( Classpath.class ); // No need for heading slash as we ask a ClassLoader instance for Resources // Would have been needed if we asked a Class instance for Resources private static final String META_INF_RESOURCES = "META-INF/resources"; /** * Serve static files from META-INF/resources in classpath. * <p> * If a directory is requested, filenames set in the <code>werval.controllers.classpath.index</code> config property * are used to find an index file. Default value is <strong>no index file support</strong>. * * @param path Path of the requested resources, relative to META-INF/resources * * @return A Chunked Outcome if found, 404 otherwise */ public Outcome metainf( String path ) { return resource( META_INF_RESOURCES + '/' + withoutHead( path, "/" ) ); } /** * Serve static resources from classpath. * <p> * If a directory is requested, filenames set in the <code>werval.controllers.classpath.index</code> config property * are used to find an index file. Default value is <strong>no index file support</strong>. * * @param basepath Base path of the requested resources, relative to the classpath root * @param path Path of the requested resources, relative to the basePath parameter * * @return A Chunked Outcome if found, 404 otherwise */ public Outcome resource( String basepath, String path ) { return resource( withoutTrail( basepath, "/" ) + '/' + withoutHead( path, "/" ) ); } /** * Serve static resources from classpath. * <p> * If a directory is requested, filenames set in the <code>werval.controllers.classpath.index</code> config property * are used to find an index file. Default value is <strong>no index file support</strong>. * * @param path Path of the requested resource, relative to the classpath root * * @return A Chunked Outcome if found, 404 otherwise */ public Outcome resource( String path ) { ensureNotEmpty( "Path", path ); path = withoutHead( path, "/" ); if( path.contains( ".." ) ) { LOG.warn( "Directory traversal attempt: '{}'", path ); return outcomes(). badRequest(). as( "text/plain" ). withBody( "Did you just attempted a directory traversal attack? Keep out." ). build(); } if( !resourceExists( application().classLoader(), path ) ) { List<String> indexFileNames = application().config().stringList( "werval.controllers.classpath.index" ); for( String indexFileName : indexFileNames ) { String indexPath = path + "/" + indexFileName; if( resourceExists( application().classLoader(), indexPath ) ) { path = indexPath; break; } } } if( !resourceExists( application().classLoader(), path ) ) { LOG.debug( "Requested resource '{}' not found", path ); return outcomes(). notFound(). as( "text/plain" ). withBody( request().path() + " not found" ). build(); } // Cache-Control if( application().mode() == Mode.DEV ) { response().headers().with( CACHE_CONTROL, "no-cache" ); } else { Long maxAge = application().config().seconds( "werval.controllers.classpath.cache.maxage" ); if( maxAge.equals( 0L ) ) { response().headers().with( CACHE_CONTROL, "no-cache" ); } else { response().headers().with( CACHE_CONTROL, "max-age=" + maxAge ); } } // Mime Type String mimetype = mimeTypes().ofPathWithCharset( path ); response().headers().with( CONTENT_TYPE, mimetype ); // Disposition and filename String resourceName = path.substring( path.lastIndexOf( '/' ) + 1 ); String filename = US_ASCII.newEncoder().canEncode( resourceName ) ? "; filename=\"" + resourceName + "\"" : "; filename*=" + application().defaultCharset().name().toLowerCase( US ) + "; filename=\"" + resourceName + "\""; if( APPLICATION_OCTET_STREAM.equals( mimetype ) ) { // Browser will prompt the user, we should provide a filename response().headers().with( "Content-Disposition", "attachment" + filename ); } else { // Tell the browser to attempt to display the file and provide a filename in case it cannot response().headers().with( "Content-Disposition", "inline" + filename ); } LOG.trace( "Will serve '{}' with mimetype '{}'", path, mimetype ); return outcomes().ok().withBody( application().classLoader().getResourceAsStream( path ) ).build(); } }