Servlet per servire contenuto statico


145

Distribuisco una webapp su due contenitori diversi (Tomcat e Jetty), ma i loro servlet predefiniti per servire il contenuto statico hanno un modo diverso di gestire la struttura dell'URL che voglio usare ( dettagli ).

Sto quindi cercando di includere un piccolo servlet nella webapp per servire il suo contenuto statico (immagini, CSS, ecc.). Il servlet dovrebbe avere le seguenti proprietà:

  • Nessuna dipendenza esterna
  • Semplice e affidabile
  • Supporto per If-Modified-Sinceintestazione (ovvero getLastModifiedmetodo personalizzato )
  • (Opzionale) supporto per codifica gzip, etags, ...

Un tale servlet è disponibile da qualche parte? Il più vicino che riesco a trovare è l' esempio 4-10 del libro servlet.

Aggiornamento: la struttura dell'URL che voglio usare - nel caso ve lo stiate chiedendo - è semplicemente:

    <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>

Quindi tutte le richieste devono essere passate al servlet principale, a meno che non siano per il staticpercorso. Il problema è che il servlet predefinito di Tomcat non tiene conto di ServletPath (quindi cerca i file statici nella cartella principale), mentre Jetty lo fa (quindi appare nella staticcartella).


Potresti approfondire la "struttura URL" che desideri utilizzare? Rotolare il tuo, basato sull'esempio collegato 4-10, sembra uno sforzo banale. L'ho fatto da solo un sacco di volte ...
— Stu Thompson,

Ho modificato la mia domanda per elaborare la struttura dell'URL. E sì, ho finito per rotolare il mio servlet. Vedi la mia risposta qui sotto.
— Bruno De Fraine,

1
Perché non usi il server web per contenuti statici?
— Stephen,

4
@Stephen: perché non c'è sempre un Apache davanti al Tomcat / Jetty. E per evitare il fastidio di una configurazione separata. Ma hai ragione, potrei considerare questa opzione.
— Bruno De Fraine,

Non riesco proprio a capire perché non hai usato la mappatura come questa <servlet-mapping> <servlet-name> default </servlet-name> <url-pattern> / </url-pattern> </ servlet-mapping > per fornire contenuto statico
— Maciek Kreft,

Risposte:


53

Ho trovato una soluzione leggermente diversa. È un po 'hack-ish, ma ecco la mappatura:

<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>

Questo fondamentalmente mappa tutti i file di contenuto per estensione al servlet predefinito e tutto il resto su "myAppServlet".

Funziona in Jetty e Tomcat.


13
in realtà puoi aggiungere più di un tag di pattern url all'interno della servelet-mapping;)
— Fareed Alnamrouti,

5
Servlet 2.5 e
— versioni

Fai solo attenzione con i file di indice (index.html) poiché potrebbero avere la precedenza sul servlet.
— Andres,

Penso che sia una cattiva idea *.sth. Se qualcuno riceverà l'URL example.com/index.jsp?g=.sthotterrà la fonte del file jsp. O mi sbaglio? (Sono nuovo in Java EE) Di solito uso il pattern url /css/*ed ecc.
— SemprePerito

46

In questo caso non è necessaria un'implementazione completamente personalizzata del servlet predefinito, è possibile utilizzare questo semplice servlet per racchiudere la richiesta nell'implementazione del contenitore:


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

Questa domanda ha un modo preciso di mappare / su un controller e / da statico a contenuto statico usando un filtro. Controlla la risposta votata dopo quella accettata: stackoverflow.com/questions/870150/…
— David Carboni,


30

Ho ottenuto buoni risultati con FileServlet , in quanto supporta praticamente tutto l'HTTP (etags, chunking, ecc.).


Grazie! ore di tentativi falliti e risposte sbagliate, e questo ha risolto il mio problema
— Yossi Shasho,

4
Sebbene per servire il contenuto da una cartella esterna all'app (lo uso per server una cartella dal disco, dì C: \ risorse) ho modificato questa riga: this.basePath = getServletContext (). GetRealPath (getInitParameter ("basePath ")); E lo ha sostituito con: this.basePath = getInitParameter ("basePath");
— Yossi Shasho,

1
Una versione aggiornata è disponibile su showcase.omnifaces.org/servlets/FileServlet
— koppor

26

Modello astratto per un servlet di risorse statiche

In parte basata su questo blog a partire dal 2007, ecco un modello astratto modernizzato e altamente riutilizzabile per un servlet che si occupa correttamente con caching, ETag, If-None-Matche If-Modified-Since(ma nessun supporto Gzip e Range, solo per mantenere le cose semplici; Gzip potrebbe essere fatto con un filtro o tramite configurazione del contenitore).

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

}

Usalo insieme all'interfaccia sottostante che rappresenta una risorsa statica.

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;

}

Tutto ciò che serve è semplicemente estendersi dal servlet astratto dato e implementare il getStaticResource()metodo secondo il javadoc.

Esempio concreto che serve dal file system:

Ecco un esempio concreto che lo serve tramite un URL come /files/foo.extdal file system del disco locale:

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

}

Esempio concreto pubblicato dal database:

Ecco un esempio concreto che lo serve tramite un URL come /files/foo.extdal database tramite una chiamata di servizio EJB che restituisce la tua entità con una byte[] contentproprietà:

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

}

1
Caro @BalusC Credo che il tuo approccio è è vulnerabile a un hacker che inviare la seguente richiesta potrebbe navigare attraverso il file system: files/%2e%2e/mysecretfile.txt. Questa richiesta produce files/../mysecretfile.txt. L'ho provato su Tomcat 7.0.55. Lo chiamano una directory climbing: owasp.org/index.php/Path_Traversal
— Cristian Arteaga

1
@Cristian: Sì, possibile. Ho aggiornato l'esempio per mostrare come prevenirlo.
— BalusC,

Questo non dovrebbe ottenere voti positivi. Fornire file statici per una pagina Web con Servlet come questo è una ricetta per la sicurezza delle catastrofi. Tutti questi problemi sono già stati risolti e non c'è motivo di implementare un nuovo modo personalizzato con il lancio di bombe a tempo di sicurezza probabilmente più sconosciute. Il percorso corretto è configurare Tomcat / GlassFish / Jetty ecc per servire il contenuto, o ancora meglio usare un file server dedicato come NGinX.
— Leonhard Printz,

@LeonhardPrintz: eliminerò la risposta e riferirò ai miei amici di Tomcat quando segnalerai problemi di sicurezza. Nessun problema.
— BalusC

19

Ho finito per rotolare il mio StaticServlet. Supporta la If-Modified-Sincecodifica gzip e dovrebbe essere in grado di servire anche file statici da file di guerra. Non è un codice molto difficile, ma non è nemmeno del tutto banale.

Il codice è disponibile: StaticServlet.java . Sentiti libero di commentare.

Aggiornamento: Khurram chiede informazioni sulla ServletUtilsclasse cui si fa riferimento StaticServlet. È semplicemente una classe con metodi ausiliari che ho usato per il mio progetto. L'unico metodo necessario è coalesce(che è identico alla funzione SQL COALESCE). Questo è il codice:

public static <T> T coalesce(T...ts) {
    for(T t: ts)
        if(t != null)
            return t;
    return null;
}

2
Non nominare il tuo errore di classe interna. Ciò potrebbe causare confusione poiché puoi confonderlo con java.lang.Error Inoltre, il tuo web.xml è lo stesso?
— Leonel,

Grazie per l'avviso di errore. web.xml è lo stesso, con "default" sostituito dal nome di StaticServlet.
— Bruno De Fraine,

1
Per quanto riguarda il metodo di coalescenza, può essere sostituito (all'interno della classe Servlet) da StringUtils.defaultString (String, String)
— Mike Minicki,

Il metodo transferStreams () può anche essere sostituito con Files.copy (is, os);
— Gerrit Brink,

Perché questo approccio è così popolare? Perché le persone stanno reimplementando file server statici come questo? Ci sono così tante falle di sicurezza che aspettano solo di essere scoperte e così tante funzionalità di veri file server statici che non sono implementate.
— Leonhard Printz,

12

A giudicare dalle informazioni di esempio sopra, penso che questo intero articolo sia basato su un comportamento corretto in Tomcat 6.0.29 e precedenti. Vedi https://issues.apache.org/bugzilla/show_bug.cgi?id=50026 . Esegui l'upgrade a Tomcat 6.0.30 e il comportamento tra (Tomcat | Jetty) dovrebbe fondersi.


1
Questa è anche la mia comprensione da svn diff -c1056763 http://svn.apache.org/repos/asf/tomcat/tc6.0.x/trunk/. Finalmente, dopo aver segnato questo WONTFIX +3 anni fa!
— Bruno De Fraine,

12

prova questo

<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>    

Modifica: valido solo per le specifiche servlet 2.5 e successive.


Sembra che questa non sia una configurazione valida.
— Gedrox,

10

Ho avuto lo stesso problema e l'ho risolto utilizzando il codice del 'servlet predefinito' dalla base di codici Tomcat.

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

Il DefaultServlet è la servlet che serve le risorse statiche (jpg, HTML, CSS, gif ecc) in Tomcat.

Questo servlet è molto efficiente e ha alcune proprietà che hai definito sopra.

Penso che questo codice sorgente sia un buon modo per avviare e rimuovere le funzionalità o le dipendenze non necessarie.

  • I riferimenti al pacchetto org.apache.naming.resources possono essere rimossi o sostituiti con il codice java.io.File.
  • I riferimenti al pacchetto org.apache.catalina.util sono probabilmente solo metodi / classi di utilità che possono essere duplicati nel codice sorgente.
  • I riferimenti alla classe org.apache.catalina.Globals possono essere incorporati o rimossi.

Sembra dipendere da molte cose da org.apache.*. Come puoi usarlo con Jetty?
— Bruno De Fraine,

Hai ragione, questa versione ha troppe dipendenze dal Tomcat (e supporta anche molte cose che potresti non desiderare.
— Modificherò la


4

L'ho fatto estendendo Tomcat DefaultServlet ( src ) e sovrascrivendo il metodo 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);
   }
}

... Ed ecco i miei mapping 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>  

1

Per servire tutte le richieste da un'app Spring, nonché /favicon.ico e i file JSP da / WEB-INF / jsp / * richiesti da SpringUsBUsView di Spring, puoi semplicemente rimappare il servlet jsp e il servlet predefinito:

  <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>

Non possiamo fare affidamento sul modello di URL * .jsp sulla mappatura standard per il servlet jsp perché il modello di percorso '/ *' viene confrontato prima di controllare qualsiasi mappatura di estensione. Mappare il servlet jsp su una cartella più profonda significa che prima è abbinato. La corrispondenza "/favicon.ico" avviene esattamente prima della corrispondenza del modello di percorso. Corrispondenze di percorso più profonde funzioneranno o corrispondenze esatte, ma nessuna corrispondenza di estensione può superare la corrispondenza del percorso '/ *'. Il mapping '/' al servlet predefinito non sembra funzionare. Penseresti che l'esatto '/' avrebbe battuto il percorso '/ *' su springapp.

La soluzione di filtro sopra non funziona per le richieste JSP inoltrate / incluse dall'applicazione. Per farlo funzionare ho dovuto applicare direttamente il filtro a springapp, a quel punto la corrispondenza del pattern url era inutile poiché tutte le richieste che vanno all'applicazione vanno anche ai suoi filtri. Quindi ho aggiunto il pattern matching al filtro e poi ho imparato a conoscere il servlet 'jsp' e ho visto che non rimuove il prefisso del percorso come fa il servlet predefinito. Ciò ha risolto il mio problema, che non era esattamente lo stesso ma abbastanza comune.


1

Controllato per Tomcat 8.x: le risorse statiche funzionano correttamente se la mappa del servlet root su "". Per servlet 3.x potrebbe essere eseguito da@WebServlet("")


0

Usa org.mortbay.jetty.handler.ContextHandler. Non hai bisogno di componenti aggiuntivi come StaticServlet.

A casa del molo,

$ cd contesti

$ 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>

Imposta il valore di contextPath con il tuo prefisso URL e imposta il valore di resourceBase come percorso del file del contenuto statico.

Ha funzionato per me.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.