Question

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?

Was it helpful?

Solution

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>

OTHER TIPS

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);
    }

}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top