I have a JavaEE application running on Wildfly 8.0.0 Final.
The application uses a lot of images and I don't want to store them in the database, so they are written to the hard disk.
How can I configure Wildfly/Undertow in order to serve these files (/var/images) on a certain URL, for example http://localhost:8080/myapplication/imagesFromDisk
?
Static content is any content that can be delivered to an end user without having to be generated, modified, or processed. The server delivers the same file to each user, making static content one of the simplest and most efficient content types to transmit over the Internet.
Use the script <WildFly directory>\bin\standalone. bat to start the WildFly server and check the installation. After startup, you should be able to access the web server at http://localhost:8080. Open the link Administration Console and follow the instructions to add a new management user.
Add another file handler and another location to the undertow subsystem in standalone.xml:
<server name="default-server"> <http-listener name="default" socket-binding="http"/> <host name="default-host" alias="localhost"> <location name="/" handler="welcome-content"/> <location name="/img" handler="images"/> </host> </server> <handlers> <file name="welcome-content" path="${jboss.home.dir}/welcome-content" directory-listing="true"/> <file name="images" path="/var/images" directory-listing="true"/> </handlers>
In case you don't want to, or can't, configure the Undertow subsystem in standalone.xml
, then you need to make it work within the .war
. For that, you may use a servlet to serve the files. There are few implementations around, the most famous (or highest on Google) is by BalusC. That one is licensed under LGPL, though. So I took the DefaultServlet
and made it more easily extensible, so you can feed it the right Undertow Resource
, e.g. from a file system.
Theoretically, some servlet in Undertow should be able to serve from a filesystem, too, but the DefaultServlet
code seems to only count with resources within a deployment.
Base class:
/* * JBoss, Home of Professional Open Source. * Copyright 2014 Red Hat, Inc., and individual contributors * as indicated by the @author tags. * * 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.undertow.servlet.handlers; import io.undertow.io.IoCallback; import io.undertow.io.Sender; import io.undertow.server.HttpServerExchange; import io.undertow.server.handlers.resource.DirectoryUtils; import io.undertow.server.handlers.resource.RangeAwareResource; import io.undertow.server.handlers.resource.Resource; import io.undertow.server.handlers.resource.ResourceManager; import io.undertow.servlet.api.DefaultServletConfig; import io.undertow.servlet.api.Deployment; import io.undertow.servlet.spec.ServletContextImpl; import io.undertow.util.ByteRange; import io.undertow.util.CanonicalPathUtils; import io.undertow.util.DateUtils; import io.undertow.util.ETag; import io.undertow.util.ETagUtils; import io.undertow.util.Headers; import io.undertow.util.Methods; import io.undertow.util.StatusCodes; import javax.servlet.DispatcherType; import javax.servlet.RequestDispatcher; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Set; /** * Abstract default servlet implementation for serving static content. * This is both a handler and a servlet. If no filters * match the current path then the content will be served up asynchronously using the * {@link io.undertow.server.HttpHandler#handleRequest(io.undertow.server.HttpServerExchange)} method, * otherwise the request is handled as a normal servlet request. * <p> * By default we only allow a restricted set of extensions. * <p> * * @author Stuart Douglas * @author Ondrej Zizka, zizka at seznam.cz */ public abstract class DefaultServlet extends HttpServlet { public static final String DIRECTORY_LISTING = "directory-listing"; public static final String DEFAULT_ALLOWED = "default-allowed"; public static final String ALLOWED_EXTENSIONS = "allowed-extensions"; public static final String DISALLOWED_EXTENSIONS = "disallowed-extensions"; public static final String RESOLVE_AGAINST_CONTEXT_ROOT = "resolve-against-context-root"; private static final Set<String> DEFAULT_ALLOWED_EXTENSIONS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( "js", "css", "png", "jpg", "gif", "html", "htm", "txt", "pdf", "jpeg", "xml", "svg", "ttf", "java"))); private Deployment deployment; private ResourceManager resourceManager; private final Settings settings = new Settings(); public Settings getSettings() { return settings; } @Override public void init(ServletConfig config) throws ServletException { super.init(config); ServletContextImpl sc = (ServletContextImpl) config.getServletContext(); this.deployment = sc.getDeployment(); // Settings from the servlet init parameters. if (config.getInitParameter(DEFAULT_ALLOWED) != null) { settings.defaultAllowed = Boolean.parseBoolean(config.getInitParameter(DEFAULT_ALLOWED)); } if (config.getInitParameter(ALLOWED_EXTENSIONS) != null) { String extensions = config.getInitParameter(ALLOWED_EXTENSIONS); settings.allowed = new HashSet<>(Arrays.asList(extensions.split(","))); } if (config.getInitParameter(DISALLOWED_EXTENSIONS) != null) { String extensions = config.getInitParameter(DISALLOWED_EXTENSIONS); settings.disallowed = new HashSet<>(Arrays.asList(extensions.split(","))); } if (config.getInitParameter(RESOLVE_AGAINST_CONTEXT_ROOT) != null) { settings.resolveAgainstContextRoot = Boolean.parseBoolean(config.getInitParameter(RESOLVE_AGAINST_CONTEXT_ROOT)); } String listings = config.getInitParameter(DIRECTORY_LISTING); if (Boolean.valueOf(listings)) { settings.directoryListingEnabled = true; } // Settings from the deployment configuration. DefaultServletConfig defaultServletConfig = getDeployment().getDeploymentInfo().getDefaultServletConfig(); if (defaultServletConfig != null) { getSettings().setDefaultAllowed(defaultServletConfig.isDefaultAllowed()); getSettings().setAllowed(new HashSet<>()); if (defaultServletConfig.getAllowed() != null) { getSettings().getAllowed().addAll(defaultServletConfig.getAllowed()); } getSettings().setDisallowed(new HashSet<>()); if (defaultServletConfig.getDisallowed() != null) { getSettings().getDisallowed().addAll(defaultServletConfig.getDisallowed()); } } } @Override protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { String path = getPath(req); if (!isAllowed(path, req.getDispatcherType())) { resp.sendError(StatusCodes.NOT_FOUND); return; } if(File.separatorChar != '/') { //if the separator char is not / we want to replace it with a / and canonicalise path = CanonicalPathUtils.canonicalize(path.replace(File.separatorChar, '/')); } final Resource resource; //we want to disallow windows characters in the path if(File.separatorChar == '/' || !path.contains(File.separator)) { resource = resourceManager.getResource(path); } else { resource = null; } if (resource == null) { if (req.getDispatcherType() == DispatcherType.INCLUDE) { //servlet 9.3 throw new FileNotFoundException(path); } else { resp.sendError(StatusCodes.NOT_FOUND); } return; } // Directory if (resource.isDirectory()) { if (!settings.directoryListingEnabled) resp.sendError(StatusCodes.FORBIDDEN); // /servlet-name/some/dir/?css or ?js if ("css".equals(req.getQueryString())) { resp.setContentType("text/css"); resp.getWriter().write(DirectoryUtils.Blobs.FILE_CSS); return; } else if ("js".equals(req.getQueryString())) { resp.setContentType("application/javascript"); resp.getWriter().write(DirectoryUtils.Blobs.FILE_JS); return; } StringBuilder output = DirectoryUtils.renderDirectoryListing(req.getRequestURI(), resource); resp.getWriter().write(output.toString()); } // File else { if(path.endsWith("/")) { //UNDERTOW-432 resp.sendError(StatusCodes.NOT_FOUND); return; } serveFileBlocking(req, resp, resource); } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { /* * Where a servlet has received a POST request we still require the capability to include static content. */ switch (req.getDispatcherType()) { case INCLUDE: case FORWARD: case ERROR: doGet(req, resp); break; default: super.doPost(req, resp); } } @Override protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { switch (req.getDispatcherType()) { case INCLUDE: case FORWARD: case ERROR: doGet(req, resp); break; default: super.doPut(req, resp); } } @Override protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { switch (req.getDispatcherType()) { case INCLUDE: case FORWARD: case ERROR: doGet(req, resp); break; default: super.doDelete(req, resp); } } @Override protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { switch (req.getDispatcherType()) { case INCLUDE: case FORWARD: case ERROR: doGet(req, resp); break; default: super.doOptions(req, resp); } } @Override protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { switch (req.getDispatcherType()) { case INCLUDE: case FORWARD: case ERROR: doGet(req, resp); break; default: super.doTrace(req, resp); } } protected void serveFileBlocking(final HttpServletRequest req, final HttpServletResponse resp, final Resource resource) throws IOException { final ETag etag = resource.getETag(); final Date lastModified = resource.getLastModified(); if(req.getDispatcherType() != DispatcherType.INCLUDE) { if (!ETagUtils.handleIfMatch(req.getHeader(Headers.IF_MATCH_STRING), etag, false) || !DateUtils.handleIfUnmodifiedSince(req.getHeader(Headers.IF_UNMODIFIED_SINCE_STRING), lastModified)) { resp.setStatus(StatusCodes.PRECONDITION_FAILED); return; } if (!ETagUtils.handleIfNoneMatch(req.getHeader(Headers.IF_NONE_MATCH_STRING), etag, true) || !DateUtils.handleIfModifiedSince(req.getHeader(Headers.IF_MODIFIED_SINCE_STRING), lastModified)) { resp.setStatus(StatusCodes.NOT_MODIFIED); return; } } //we are going to proceed. Set the appropriate headers if(resp.getContentType() == null) { if(!resource.isDirectory()) { final String contentType = deployment.getServletContext().getMimeType(resource.getName()); if (contentType != null) { resp.setContentType(contentType); } else { resp.setContentType("application/octet-stream"); } } } if (lastModified != null) { resp.setHeader(Headers.LAST_MODIFIED_STRING, resource.getLastModifiedString()); } if (etag != null) { resp.setHeader(Headers.ETAG_STRING, etag.toString()); } ByteRange range = null; long start = -1, end = -1; try { //only set the content length if we are using a stream //if we are using a writer who knows what the length will end up being //todo: if someone installs a filter this can cause problems //not sure how best to deal with this //we also can't deal with range requests if a writer is in use Long contentLength = resource.getContentLength(); if (contentLength != null) { resp.getOutputStream(); if(contentLength > Integer.MAX_VALUE) { resp.setContentLengthLong(contentLength); } else { resp.setContentLength(contentLength.intValue()); } if(resource instanceof RangeAwareResource && ((RangeAwareResource)resource).isRangeSupported()) { //TODO: figure out what to do with the content encoded resource manager range = ByteRange.parse(req.getHeader(Headers.RANGE_STRING)); if(range != null && range.getRanges() == 1) { start = range.getStart(0); end = range.getEnd(0); if(start == -1 ) { //suffix range long toWrite = end; if(toWrite >= 0) { if(toWrite > Integer.MAX_VALUE) { resp.setContentLengthLong(toWrite); } else { resp.setContentLength((int)toWrite); } } else { //ignore the range request range = null; } start = contentLength - end; end = contentLength; } else if(end == -1) { //prefix range long toWrite = contentLength - start; if(toWrite >= 0) { if(toWrite > Integer.MAX_VALUE) { resp.setContentLengthLong(toWrite); } else { resp.setContentLength((int)toWrite); } } else { //ignore the range request range = null; } end = contentLength; } else { long toWrite = end - start + 1; if(toWrite > Integer.MAX_VALUE) { resp.setContentLengthLong(toWrite); } else { resp.setContentLength((int)toWrite); } } if(range != null) { resp.setStatus(StatusCodes.PARTIAL_CONTENT); resp.setHeader(Headers.CONTENT_RANGE_STRING, range.getStart(0) + "-" + range.getEnd(0) + "/" + contentLength); } } } } } catch (IllegalStateException e) { } final boolean include = req.getDispatcherType() == DispatcherType.INCLUDE; if (!req.getMethod().equals(Methods.HEAD_STRING)) { HttpServerExchange exchange = SecurityActions.requireCurrentServletRequestContext().getOriginalRequest().getExchange(); if(range == null) { resource.serve(exchange.getResponseSender(), exchange, completionCallback(include)); } else { ((RangeAwareResource)resource).serveRange(exchange.getResponseSender(), exchange, start, end, completionCallback(include)); } } } private IoCallback completionCallback(final boolean include) { return new IoCallback() { @Override public void onComplete(final HttpServerExchange exchange, final Sender sender) { if (!include) { sender.close(); } } @Override public void onException(final HttpServerExchange exchange, final Sender sender, final IOException exception) { //not much we can do here, the connection is broken sender.close(); } }; } /** * Returns the canonicalized URL path. */ private String getPath(final HttpServletRequest request) { String servletPath; String pathInfo; if (request.getDispatcherType() == DispatcherType.INCLUDE && request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null) { pathInfo = (String) request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO); servletPath = (String) request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); } else { pathInfo = request.getPathInfo(); servletPath = request.getServletPath(); } String result = pathInfo; if (result == null) { result = servletPath; } else if(settings.resolveAgainstContextRoot) { result = servletPath + CanonicalPathUtils.canonicalize(pathInfo); } else { result = CanonicalPathUtils.canonicalize(result); } if ((result == null) || (result.equals(""))) { result = "/"; } return result; } /** * @return true if the resource at the given path is allowed to be sent to the client. */ protected boolean isAllowed(String path, DispatcherType dispatcherType) { int pos = path.lastIndexOf('/'); final String lastSegment; if (pos == -1) { lastSegment = path; } else { lastSegment = path.substring(pos + 1); } if (lastSegment.isEmpty()) { return true; } int ext = lastSegment.lastIndexOf('.'); if (ext == -1) { //no extension return true; } final String extension = lastSegment.substring(ext + 1, lastSegment.length()); if (settings.defaultAllowed) { return !settings.disallowed.contains(extension); } else { return settings.allowed.contains(extension); } } public boolean isDirectoryListingEnabled() { return settings.directoryListingEnabled; } public void setDirectoryListingEnabled(boolean directoryListingEnabled) { settings.directoryListingEnabled = directoryListingEnabled; } public ResourceManager getResourceManager() { return resourceManager; } public void setResourceManager(ResourceManager resourceManager) { this.resourceManager = resourceManager; } public Deployment getDeployment() { return deployment; } protected static class Settings { private boolean directoryListingEnabled = false; private boolean defaultAllowed = true; private Set<String> allowed = DEFAULT_ALLOWED_EXTENSIONS; private Set<String> disallowed = Collections.emptySet(); private boolean resolveAgainstContextRoot; public boolean isDirectoryListingEnabled() { return directoryListingEnabled; } public void setDirectoryListingEnabled(boolean directoryListingEnabled) { this.directoryListingEnabled = directoryListingEnabled; } public boolean isDefaultAllowed() { return defaultAllowed; } public void setDefaultAllowed(boolean defaultAllowed) { this.defaultAllowed = defaultAllowed; } public Set<String> getAllowed() { return allowed; } public void setAllowed(Set<String> allowed) { this.allowed = allowed; } public Set<String> getDisallowed() { return disallowed; } public void setDisallowed(Set<String> disallowed) { this.disallowed = disallowed; } public boolean isResolveAgainstContextRoot() { return resolveAgainstContextRoot; } public void setResolveAgainstContextRoot(boolean resolveAgainstContextRoot) { this.resolveAgainstContextRoot = resolveAgainstContextRoot; } } }
The file serving servlet (the one you will use in web.xml
):
/* * JBoss, Home of Professional Open Source. * Copyright 2014 Red Hat, Inc., and individual contributors * as indicated by the @author tags. * * 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 org.jboss.windup.web.fileservlet; import io.undertow.Undertow; import java.util.logging.Logger; import io.undertow.server.handlers.resource.FileResourceManager; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import java.io.File; import org.jboss.windup.web.addons.websupport.WebPathUtil; /** * Default servlet responsible for serving up files from a directory. * This is both a handler and a servlet. If no filters * match the current path, then the resources will be served up asynchronously using the * {@link io.undertow.server.HttpHandler#handleRequest(io.undertow.server.HttpServerExchange)} method, * otherwise the request is handled as a normal servlet request. * * <p> * By default we only allow a restricted set of extensions. * <p> * @author Ondrej Zizka, zizka at seznam.cz * @see Undertow's DefaultServlet */ public class FileDefaultServlet extends DefaultServlet { public static final String BASE_PATH = "base-path"; /** * Which directory should this servlet serve files from. */ private String basePath; @Override public void init(ServletConfig config) throws ServletException { super.init(config); initBasePath(); this.setResourceManager(new FileResourceManager(new File(basePath), 8*1024)); } private void initBasePath() throws ServletException { // Get base path (path to get all resources from) as init parameter. this.basePath = getInitParameter(BASE_PATH); this.basePath = WebPathUtil.expandVariables(this.basePath); // Validate base path. if (this.basePath == null) { throw new ServletException("FileServlet init param 'basePath' is required."); } else { File path = new File(this.basePath); if (!path.exists()) { throw new ServletException("FileServlet init param 'basePath' value '" + this.basePath + "' does actually not exist in file system."); } else if (!path.isDirectory()) { throw new ServletException("FileServlet init param 'basePath' value '" + this.basePath + "' is actually not a directory in file system."); } else if (!path.canRead()) { throw new ServletException("FileServlet init param 'basePath' value '" + this.basePath + "' is actually not readable in file system."); } } } }
And here's a servlet which works like the previous DefaultServlet
- that is, serving from the deployment.
/* * JBoss, Home of Professional Open Source. * Copyright 2014 Red Hat, Inc., and individual contributors * as indicated by the @author tags. * * 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.undertow.servlet.handlers; import javax.servlet.DispatcherType; import javax.servlet.ServletConfig; import javax.servlet.ServletException; /** * Default servlet responsible for serving up resources. This is both a handler and a servlet. If no filters * match the current path then the resources will be served up asynchronously using the * {@link io.undertow.server.HttpHandler#handleRequest(io.undertow.server.HttpServerExchange)} method, * otherwise the request is handled as a normal servlet request. * <p> * By default we only allow a restricted set of extensions. * <p> * todo: this thing needs a lot more work. In particular: * - caching for blocking requests * - correct mime type * - range/last-modified and other headers to be handled properly * - head requests * - and probably heaps of other things * * @author Ondrej Zizka, zizka at seznam.cz */ public class ResourceDefaultServlet extends DefaultServlet { @Override public void init(ServletConfig config) throws ServletException { super.init(config); this.setResourceManager(getDeployment().getDeploymentInfo().getResourceManager()); } @Override protected boolean isAllowed(String path, DispatcherType dispatcherType) { if (!path.isEmpty()) { if(dispatcherType == DispatcherType.REQUEST) { //WFLY-3543 allow the dispatcher to access stuff in web-inf and meta inf if (path.startsWith("/META-INF") || path.startsWith("META-INF") || path.startsWith("/WEB-INF") || path.startsWith("WEB-INF")) { return false; } } } return super.isAllowed(path, dispatcherType); } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With