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.