Servlet pour servir du contenu statique

145

Je déploie une webapp sur deux conteneurs différents (Tomcat et Jetty), mais leurs servlets par défaut pour servir le contenu statique ont une manière différente de gérer la structure d'URL que je souhaite utiliser ( détails ).

Je cherche donc à inclure un petit servlet dans la webapp pour servir son propre contenu statique (images, CSS, etc.). Le servlet doit avoir les propriétés suivantes:

  • Aucune dépendance externe
  • Simple et fiable
  • Prise en charge de l'en- If-Modified-Sincetête (c'est- à -dire getLastModifiedméthode personnalisée )
  • (Facultatif) Prise en charge de l'encodage gzip, etags, ...

Un tel servlet est-il disponible quelque part? Le plus proche que je puisse trouver est l' exemple 4-10 du livre de servlet.

Mise à jour: La structure d'URL que je souhaite utiliser - au cas où vous vous poseriez la question - est simplement:

    <servlet-mapping>
            <servlet-name>main</servlet-name>
            <url-pattern>/*</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
            <servlet-name>default</servlet-name>
            <url-pattern>/static/*</url-pattern>
    </servlet-mapping>

Ainsi, toutes les requêtes doivent être passées au servlet principal, sauf si elles concernent le staticchemin. Le problème est que le servlet par défaut de Tomcat ne prend pas en compte le ServletPath (il recherche donc les fichiers statiques dans le dossier principal), alors que Jetty le fait (il regarde donc dans le staticdossier).

Bruno De Fraine
la source
Pourriez-vous nous expliquer la "structure URL" que vous souhaitez utiliser? Rouler le vôtre, basé sur l'exemple lié 4-10, semble être un effort trivial. Je l'ai fait moi-même beaucoup de fois ...
Stu Thompson
J'ai modifié ma question pour élaborer la structure de l'URL. Et oui, j'ai fini par rouler ma propre servlet. Voir ma réponse ci-dessous.
Bruno De Fraine
1
Pourquoi n'utilisez-vous pas le serveur Web pour le contenu statique?
Stephen
4
@Stephen: car il n'y a pas toujours d'Apache devant le Tomcat / Jetty. Et pour éviter les tracas d'une configuration séparée. Mais vous avez raison, je pourrais envisager cette option.
Bruno De Fraine
Je ne comprends tout simplement pas pourquoi vous n'avez pas utilisé de mappage comme celui-ci <servlet-mapping> <servlet-name> default </servlet-name> <url-pattern> / </url-pattern> </ servlet-mapping > pour servir du contenu statique
Maciek Kreft

Réponses:

53

J'ai trouvé une solution légèrement différente. C'est un peu hack-ish, mais voici le mapping:

<servlet-mapping>   
    <servlet-name>default</servlet-name>
    <url-pattern>*.html</url-pattern>
</servlet-mapping>
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.jpg</url-pattern>
</servlet-mapping>
<servlet-mapping>
 <servlet-name>default</servlet-name>
    <url-pattern>*.png</url-pattern>
</servlet-mapping>
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.css</url-pattern>
</servlet-mapping>
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.js</url-pattern>
</servlet-mapping>

<servlet-mapping>
    <servlet-name>myAppServlet</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

Ceci mappe simplement tous les fichiers de contenu par extension au servlet par défaut, et tout le reste à "myAppServlet".

Cela fonctionne à la fois dans Jetty et Tomcat.

Taylor Gautier
la source
13
en fait, vous pouvez ajouter plus d'une balise de modèle d'url dans le servelet-mapping;)
Fareed Alnamrouti
5
Servlet 2.5 et plus récent prennent en charge plusieurs balises de modèle d'URL dans le mappage de servlet
vivid_voidgroup
Faites juste attention aux fichiers d'index (index.html) car ils peuvent avoir la priorité sur votre servlet.
Andres
Je pense que c'est une mauvaise utilisation *.sth. Si quelqu'un obtient l'URL, example.com/index.jsp?g=.sthil obtiendra la source du fichier jsp. Ou je me trompe? (Je suis nouveau dans Java EE) J'utilise habituellement un modèle d'url, /css/*etc.
SemperPeritus
46

Il n'y a pas besoin d'une implémentation complètement personnalisée du servlet par défaut dans ce cas, vous pouvez utiliser ce simple servlet pour encapsuler la demande dans l'implémentation du conteneur:


package com.example;

import java.io.*;

import javax.servlet.*;
import javax.servlet.http.*;

public class DefaultWrapperServlet extends HttpServlet
{   
    public void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException
    {
        RequestDispatcher rd = getServletContext().getNamedDispatcher("default");

        HttpServletRequest wrapped = new HttpServletRequestWrapper(req) {
            public String getServletPath() { return ""; }
        };

        rd.forward(wrapped, resp);
    }
}
axtavt
la source
Cette question permet de mapper / à un contrôleur et / statique à un contenu statique à l'aide d'un filtre. Vérifiez la réponse votée après celle acceptée: stackoverflow.com/questions/870150/…
David Carboni
30

J'ai eu de bons résultats avec FileServlet , car il prend en charge à peu près tout HTTP (etags, chunking, etc.).

Will Hartung
la source
Merci! heures de tentatives infructueuses et de mauvaises réponses, et cela a résolu mon problème
Yossi Shasho
4
Cependant, pour servir le contenu d'un dossier en dehors de l'application (je l'utilise pour serveur un dossier à partir du disque, disons C: \ resources), j'ai modifié cette ligne: this.basePath = getServletContext (). GetRealPath (getInitParameter ("basePath ")); Et l'a remplacé par: this.basePath = getInitParameter ("basePath");
Yossi Shasho
1
Une version mise à jour est disponible sur showcase.omnifaces.org/servlets/FileServlet
koppor
26

Modèle abstrait pour un servlet de ressources statiques

En partie basé sur ce blog de 2007, voici un modèle abstrait modernisé et hautement réutilisable pour un servlet qui traite correctement la mise en cache ETag, If-None-Matchet If-Modified-Since(mais pas de support Gzip et Range; juste pour rester simple; Gzip pourrait être fait avec un filtre ou via configuration du conteneur).

public abstract class StaticResourceServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;
    private static final long ONE_SECOND_IN_MILLIS = TimeUnit.SECONDS.toMillis(1);
    private static final String ETAG_HEADER = "W/\"%s-%s\"";
    private static final String CONTENT_DISPOSITION_HEADER = "inline;filename=\"%1$s\"; filename*=UTF-8''%1$s";

    public static final long DEFAULT_EXPIRE_TIME_IN_MILLIS = TimeUnit.DAYS.toMillis(30);
    public static final int DEFAULT_STREAM_BUFFER_SIZE = 102400;

    @Override
    protected void doHead(HttpServletRequest request, HttpServletResponse response) throws ServletException ,IOException {
        doRequest(request, response, true);
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doRequest(request, response, false);
    }

    private void doRequest(HttpServletRequest request, HttpServletResponse response, boolean head) throws IOException {
        response.reset();
        StaticResource resource;

        try {
            resource = getStaticResource(request);
        }
        catch (IllegalArgumentException e) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }

        if (resource == null) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        String fileName = URLEncoder.encode(resource.getFileName(), StandardCharsets.UTF_8.name());
        boolean notModified = setCacheHeaders(request, response, fileName, resource.getLastModified());

        if (notModified) {
            response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
            return;
        }

        setContentHeaders(response, fileName, resource.getContentLength());

        if (head) {
            return;
        }

        writeContent(response, resource);
    }

    /**
     * Returns the static resource associated with the given HTTP servlet request. This returns <code>null</code> when
     * the resource does actually not exist. The servlet will then return a HTTP 404 error.
     * @param request The involved HTTP servlet request.
     * @return The static resource associated with the given HTTP servlet request.
     * @throws IllegalArgumentException When the request is mangled in such way that it's not recognizable as a valid
     * static resource request. The servlet will then return a HTTP 400 error.
     */
    protected abstract StaticResource getStaticResource(HttpServletRequest request) throws IllegalArgumentException;

    private boolean setCacheHeaders(HttpServletRequest request, HttpServletResponse response, String fileName, long lastModified) {
        String eTag = String.format(ETAG_HEADER, fileName, lastModified);
        response.setHeader("ETag", eTag);
        response.setDateHeader("Last-Modified", lastModified);
        response.setDateHeader("Expires", System.currentTimeMillis() + DEFAULT_EXPIRE_TIME_IN_MILLIS);
        return notModified(request, eTag, lastModified);
    }

    private boolean notModified(HttpServletRequest request, String eTag, long lastModified) {
        String ifNoneMatch = request.getHeader("If-None-Match");

        if (ifNoneMatch != null) {
            String[] matches = ifNoneMatch.split("\\s*,\\s*");
            Arrays.sort(matches);
            return (Arrays.binarySearch(matches, eTag) > -1 || Arrays.binarySearch(matches, "*") > -1);
        }
        else {
            long ifModifiedSince = request.getDateHeader("If-Modified-Since");
            return (ifModifiedSince + ONE_SECOND_IN_MILLIS > lastModified); // That second is because the header is in seconds, not millis.
        }
    }

    private void setContentHeaders(HttpServletResponse response, String fileName, long contentLength) {
        response.setHeader("Content-Type", getServletContext().getMimeType(fileName));
        response.setHeader("Content-Disposition", String.format(CONTENT_DISPOSITION_HEADER, fileName));

        if (contentLength != -1) {
            response.setHeader("Content-Length", String.valueOf(contentLength));
        }
    }

    private void writeContent(HttpServletResponse response, StaticResource resource) throws IOException {
        try (
            ReadableByteChannel inputChannel = Channels.newChannel(resource.getInputStream());
            WritableByteChannel outputChannel = Channels.newChannel(response.getOutputStream());
        ) {
            ByteBuffer buffer = ByteBuffer.allocateDirect(DEFAULT_STREAM_BUFFER_SIZE);
            long size = 0;

            while (inputChannel.read(buffer) != -1) {
                buffer.flip();
                size += outputChannel.write(buffer);
                buffer.clear();
            }

            if (resource.getContentLength() == -1 && !response.isCommitted()) {
                response.setHeader("Content-Length", String.valueOf(size));
            }
        }
    }

}

Utilisez-le avec l'interface ci-dessous représentant une ressource statique.

interface StaticResource {

    /**
     * Returns the file name of the resource. This must be unique across all static resources. If any, the file
     * extension will be used to determine the content type being set. If the container doesn't recognize the
     * extension, then you can always register it as <code>&lt;mime-type&gt;</code> in <code>web.xml</code>.
     * @return The file name of the resource.
     */
    public String getFileName();

    /**
     * Returns the last modified timestamp of the resource in milliseconds.
     * @return The last modified timestamp of the resource in milliseconds.
     */
    public long getLastModified();

    /**
     * Returns the content length of the resource. This returns <code>-1</code> if the content length is unknown.
     * In that case, the container will automatically switch to chunked encoding if the response is already
     * committed after streaming. The file download progress may be unknown.
     * @return The content length of the resource.
     */
    public long getContentLength();

    /**
     * Returns the input stream with the content of the resource. This method will be called only once by the
     * servlet, and only when the resource actually needs to be streamed, so lazy loading is not necessary.
     * @return The input stream with the content of the resource.
     * @throws IOException When something fails at I/O level.
     */
    public InputStream getInputStream() throws IOException;

}

Tout ce dont vous avez besoin est juste d'étendre le servlet abstrait donné et d'implémenter la getStaticResource()méthode selon le javadoc.

Exemple concret servant à partir du système de fichiers:

Voici un exemple concret qui le sert via une URL comme à /files/foo.extpartir du système de fichiers du disque local:

@WebServlet("/files/*")
public class FileSystemResourceServlet extends StaticResourceServlet {

    private File folder;

    @Override
    public void init() throws ServletException {
        folder = new File("/path/to/the/folder");
    }

    @Override
    protected StaticResource getStaticResource(HttpServletRequest request) throws IllegalArgumentException {
        String pathInfo = request.getPathInfo();

        if (pathInfo == null || pathInfo.isEmpty() || "/".equals(pathInfo)) {
            throw new IllegalArgumentException();
        }

        String name = URLDecoder.decode(pathInfo.substring(1), StandardCharsets.UTF_8.name());
        final File file = new File(folder, Paths.get(name).getFileName().toString());

        return !file.exists() ? null : new StaticResource() {
            @Override
            public long getLastModified() {
                return file.lastModified();
            }
            @Override
            public InputStream getInputStream() throws IOException {
                return new FileInputStream(file);
            }
            @Override
            public String getFileName() {
                return file.getName();
            }
            @Override
            public long getContentLength() {
                return file.length();
            }
        };
    }

}

Exemple concret servant à partir de la base de données:

Voici un exemple concret qui le sert via une URL comme /files/foo.extdepuis la base de données via un appel de service EJB qui renvoie votre entité ayant une byte[] contentpropriété:

@WebServlet("/files/*")
public class YourEntityResourceServlet extends StaticResourceServlet {

    @EJB
    private YourEntityService yourEntityService;

    @Override
    protected StaticResource getStaticResource(HttpServletRequest request) throws IllegalArgumentException {
        String pathInfo = request.getPathInfo();

        if (pathInfo == null || pathInfo.isEmpty() || "/".equals(pathInfo)) {
            throw new IllegalArgumentException();
        }

        String name = URLDecoder.decode(pathInfo.substring(1), StandardCharsets.UTF_8.name());
        final YourEntity yourEntity = yourEntityService.getByName(name);

        return (yourEntity == null) ? null : new StaticResource() {
            @Override
            public long getLastModified() {
                return yourEntity.getLastModified();
            }
            @Override
            public InputStream getInputStream() throws IOException {
                return new ByteArrayInputStream(yourEntityService.getContentById(yourEntity.getId()));
            }
            @Override
            public String getFileName() {
                return yourEntity.getName();
            }
            @Override
            public long getContentLength() {
                return yourEntity.getContentLength();
            }
        };
    }

}
BalusC
la source
1
Cher @BalusC Je pense que votre approche est vulnérable à un pirate informatique qui envoie la demande suivante pourrait naviguer creux le système de fichiers: files/%2e%2e/mysecretfile.txt. Cette demande produit files/../mysecretfile.txt. Je l'ai testé sur Tomcat 7.0.55. Ils appellent cela un répertoire grimpant: owasp.org/index.php/Path_Traversal
Cristian Arteaga
1
@Cristian: Ouais, c'est possible. J'ai mis à jour l'exemple pour montrer comment éviter cela.
BalusC
Cela ne devrait pas susciter de votes positifs. Servir des fichiers statiques pour une page Web avec un servlet comme celui-ci est une recette pour la sécurité en cas de catastrophe. Tous ces problèmes ont déjà été résolus et il n'y a aucune raison de mettre en œuvre une nouvelle méthode personnalisée avec probablement des bombes à retardement de sécurité plus inconnues. Le chemin correct est de configurer Tomcat / GlassFish / Jetty etc. pour servir le contenu, ou encore mieux d'utiliser un serveur de fichiers dédié comme NGinX.
Leonhard Printz
@LeonhardPrintz: Je supprimerai la réponse et ferai un rapport à mes amis chez Tomcat une fois que vous aurez signalé des problèmes de sécurité. Aucun problème.
BalusC
19

J'ai fini par rouler le mien StaticServlet. Il prend en charge l' If-Modified-Sinceencodage gzip et devrait également pouvoir servir des fichiers statiques à partir de fichiers war. Ce n'est pas du code très difficile, mais ce n'est pas non plus entièrement trivial.

Le code est disponible: StaticServlet.java . N'hésitez pas à commenter.

Mise à jour: Khurram pose des questions sur la ServletUtilsclasse référencée dans StaticServlet. C'est simplement une classe avec des méthodes auxiliaires que j'ai utilisées pour mon projet. La seule méthode dont vous avez besoin est coalesce(qui est identique à la fonction SQL COALESCE). Voici le code:

public static <T> T coalesce(T...ts) {
    for(T t: ts)
        if(t != null)
            return t;
    return null;
}
Bruno De Fraine
la source
2
Ne nommez pas votre classe interne Error. Cela pourrait prêter à confusion car vous pouvez le confondre avec java.lang.Error De plus, votre web.xml est-il le même?
Leonel le
Merci pour l'avertissement d'erreur. web.xml est le même, avec "default" remplacé par le nom du StaticServlet.
Bruno De Fraine
1
Quant à la méthode coalesce, elle peut être remplacée (à l'intérieur de la classe Servlet) par commons-lang StringUtils.defaultString (String, String)
Mike Minicki
La méthode transferStreams () peut également être remplacée par Files.copy (is, os);
Gerrit Brink
Pourquoi cette approche est-elle si populaire? Pourquoi les gens réimplémentent-ils des serveurs de fichiers statiques comme celui-ci? Il y a tellement de failles de sécurité qui ne demandent qu'à être découvertes, et tant de fonctionnalités de vrais serveurs de fichiers statiques qui ne sont pas implémentées.
Leonhard Printz
12

À en juger par les informations d'exemple ci-dessus, je pense que cet article entier est basé sur un comportement bogué dans Tomcat 6.0.29 et versions antérieures. Voir https://issues.apache.org/bugzilla/show_bug.cgi?id=50026 . La mise à niveau vers Tomcat 6.0.30 et le comportement entre (Tomcat | Jetty) doit fusionner.

Jeff Stice-Hall
la source
1
C'est aussi ce que j'ai compris svn diff -c1056763 http://svn.apache.org/repos/asf/tomcat/tc6.0.x/trunk/. Enfin, après avoir marqué ce WONTFIX il y a +3 ans!
Bruno De Fraine
12

essaye ça

<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.js</url-pattern>
    <url-pattern>*.css</url-pattern>
    <url-pattern>*.ico</url-pattern>
    <url-pattern>*.png</url-pattern>
    <url-pattern>*.jpg</url-pattern>
    <url-pattern>*.htc</url-pattern>
    <url-pattern>*.gif</url-pattern>
</servlet-mapping>    

Edit: Ceci n'est valable que pour la spécification servlet 2.5 et plus.

Tarif Alnamrouti
la source
Il semble que ce ne soit pas une configuration valide.
Gedrox
10

J'ai eu le même problème et je l'ai résolu en utilisant le code du 'servlet par défaut' de la base de code Tomcat.

https://github.com/apache/tomcat/blob/master/java/org/apache/catalina/servlets/DefaultServlet.java

Le DefaultServlet est le servlet qui sert les ressources statiques (jpg, html, css, gif, etc.) dans Tomcat.

Ce servlet est très efficace et possède certaines des propriétés que vous avez définies ci-dessus.

Je pense que ce code source est un bon moyen de démarrer et de supprimer les fonctionnalités ou les dépendances dont vous n'avez pas besoin.

  • Les références au package org.apache.naming.resources peuvent être supprimées ou remplacées par du code java.io.File.
  • Les références au package org.apache.catalina.util ne sont probablement que des méthodes / classes utilitaires qui peuvent être dupliquées dans votre code source.
  • Les références à la classe org.apache.catalina.Globals peuvent être insérées ou supprimées.
Panagiotis Korros
la source
Cela semble dépendre de beaucoup de choses org.apache.*. Comment pouvez-vous l'utiliser avec Jetty?
Bruno De Fraine
Vous avez raison, cette version a trop de dépendances avec le Tomcat (elle prend également en charge beaucoup de choses que vous pourriez ne pas vouloir. Je vais éditer ma réponse.
Panagiotis Korros
4

J'ai fait cela en étendant le tomcat DefaultServlet ( src ) et en remplaçant la méthode getRelativePath ().

package com.example;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import org.apache.catalina.servlets.DefaultServlet;

public class StaticServlet extends DefaultServlet
{
   protected String pathPrefix = "/static";

   public void init(ServletConfig config) throws ServletException
   {
      super.init(config);

      if (config.getInitParameter("pathPrefix") != null)
      {
         pathPrefix = config.getInitParameter("pathPrefix");
      }
   }

   protected String getRelativePath(HttpServletRequest req)
   {
      return pathPrefix + super.getRelativePath(req);
   }
}

... Et voici mes mappages de servlet

<servlet>
    <servlet-name>StaticServlet</servlet-name>
    <servlet-class>com.example.StaticServlet</servlet-class>
    <init-param>
        <param-name>pathPrefix</param-name>
        <param-value>/static</param-value>
    </init-param>       
</servlet>

<servlet-mapping>
    <servlet-name>StaticServlet</servlet-name>
    <url-pattern>/static/*</url-pattern>
</servlet-mapping>  
delux247
la source
1

Pour répondre à toutes les demandes d'une application Spring ainsi que /favicon.ico et les fichiers JSP de / WEB-INF / jsp / * que AbstractUrlBasedView de Spring demandera, vous pouvez simplement remapper le servlet jsp et le servlet par défaut:

  <servlet>
    <servlet-name>springapp</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>jsp</servlet-name>
    <url-pattern>/WEB-INF/jsp/*</url-pattern>
  </servlet-mapping>

  <servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>/favicon.ico</url-pattern>
  </servlet-mapping>

  <servlet-mapping>
    <servlet-name>springapp</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>

Nous ne pouvons pas nous fier au modèle d'url * .jsp sur le mappage standard du servlet jsp car le modèle de chemin '/ *' correspond avant que tout mappage d'extension ne soit vérifié. Le mappage du servlet jsp vers un dossier plus profond signifie qu'il est mis en correspondance en premier. La correspondance avec '/favicon.ico' se produit exactement avant la correspondance du modèle de chemin. Des correspondances de chemin plus profondes fonctionneront, ou des correspondances exactes, mais aucune correspondance d'extension ne peut dépasser la correspondance de chemin '/ *'. Le mappage de «/» au servlet par défaut ne semble pas fonctionner. On pourrait penser que le «/» exact battrait le modèle de chemin «/ *» sur springapp.

La solution de filtrage ci-dessus ne fonctionne pas pour les demandes JSP transférées / incluses de l'application. Pour que cela fonctionne, je devais appliquer le filtre directement à springapp, à quel point la correspondance url-pattern était inutile car toutes les requêtes qui vont à l'application vont également à ses filtres. J'ai donc ajouté une correspondance de modèle au filtre, puis j'ai découvert le servlet 'jsp' et j'ai vu qu'il ne supprimait pas le préfixe de chemin comme le fait le servlet par défaut. Cela a résolu mon problème, qui n'était pas exactement le même mais assez courant.


la source
1

Vérifié pour Tomcat 8.x: les ressources statiques fonctionnent correctement si le servlet racine est mappé sur "". Pour la servlet 3.x, cela pourrait être fait par@WebServlet("")

Grigory Kislin
la source
0

Utilisez org.mortbay.jetty.handler.ContextHandler. Vous n'avez pas besoin de composants supplémentaires tels que StaticServlet.

À la maison jetée,

contextes $ cd

$ cp javadoc.xml static.xml

$ vi static.xml

...

<Configure class="org.mortbay.jetty.handler.ContextHandler">
<Set name="contextPath">/static</Set>
<Set name="resourceBase"><SystemProperty name="jetty.home" default="."/>/static/</Set>
<Set name="handler">
  <New class="org.mortbay.jetty.handler.ResourceHandler">
    <Set name="cacheControl">max-age=3600,public</Set>
  </New>
 </Set>
</Configure>

Définissez la valeur de contextPath avec votre préfixe d'URL et définissez la valeur de resourceBase comme chemin de fichier du contenu statique.

Cela a fonctionné pour moi.

yogman
la source