web-dev-qa-db-fra.com

Comment configurer Wildfly pour servir du contenu statique (comme des images)?

J'ai une application JavaEE exécutée sur Wildfly 8.0.0 Final.

L'application utilise beaucoup d'images et je ne veux pas les stocker dans la base de données, elles sont donc écrites sur le disque dur.

Comment puis-je configurer Wildfly/Undertow afin de servir ces fichiers (/ var/images) sur une certaine URL, par exemple http://localhost:8080/myapplication/imagesFromDisk?

34
Christian Götz

Ajoutez un autre gestionnaire de fichiers et un autre emplacement au sous-système Undertow dans 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>
57
Harald Wellmann

Dans le cas où vous ne voulez pas, ou ne pouvez pas, configurer le sous-système Undertow in standalone.xml, vous devez le faire fonctionner dans le .war. Pour cela, vous pouvez utiliser un servlet pour servir les fichiers. Il existe peu d'implémentations, la plus célèbre (ou la plus élevée sur Google) est par BalusC . Celui-ci est sous licence LGPL, cependant. J'ai donc pris les DefaultServlet et pour les rendre plus facilement extensibles , vous pouvez donc les alimenter dans le bon Undertow Resource, par ex. à partir d'un système de fichiers .

Théoriquement, certains servlets dans Undertow devraient également pouvoir servir à partir d'un système de fichiers, mais le code DefaultServlet ne semble compter qu'avec les ressources d'un déploiement.

Classe de base:

 /*
 * 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;
    }
    }
}

Le servlet de service de fichiers (celui que vous utiliserez dans 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.");
        }
    }
    }

}

Et voici un servlet qui fonctionne comme le précédent DefaultServlet - c'est-à-dire, servant à partir du déploiement.

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

}
2
Ondra Žižka