Servlet para servir contenido estático

145

Implemento una aplicación web en dos contenedores diferentes (Tomcat y Jetty), pero sus servlets predeterminados para servir el contenido estático tienen una forma diferente de manejar la estructura de URL que quiero usar ( detalles ).

Por lo tanto, estoy buscando incluir un pequeño servlet en la aplicación web para servir su propio contenido estático (imágenes, CSS, etc.). El servlet debe tener las siguientes propiedades:

  • Sin dependencias externas
  • Simple y confiable
  • Soporte para If-Modified-Sinceencabezado (es decir, getLastModifiedmétodo personalizado )
  • (Opcional) soporte para codificación gzip, etags, ...

¿Existe un servlet de este tipo en algún lugar? Lo más cercano que puedo encontrar es el ejemplo 4-10 del libro de servlets.

Actualización: la estructura de URL que quiero usar, en caso de que se lo pregunte, es simplemente:

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

Por lo tanto, todas las solicitudes deben pasarse al servlet principal, a menos que sean para la staticruta. El problema es que el servlet predeterminado de Tomcat no tiene en cuenta ServletPath (por lo que busca los archivos estáticos en la carpeta principal), mientras que Jetty sí (por lo que se ve en la staticcarpeta).

Bruno De Fraine
fuente
¿Podría dar más detalles sobre la "estructura de URL" que desea utilizar? Hacer el suyo propio, basado en el ejemplo vinculado 4-10, parece un esfuerzo trivial. Lo he hecho muchas veces ...
Stu Thompson
Edité mi pregunta para elaborar la estructura de URL. Y sí, terminé rodando mi propio servlet. Vea mi respuesta a continuación.
Bruno De Fraine
1
¿Por qué no usas el servidor web para contenido estático?
Stephen
44
@Stephen: porque no siempre hay un Apache frente al Tomcat / Jetty. Y para evitar la molestia de una configuración separada. Pero tienes razón, podría considerar esa opción.
Bruno De Fraine
Simplemente no puedo entender, por qué no usaste mapeos como este <servlet-mapping> <servlet-name> default </servlet-name> <url-pattern> / </url-pattern> </ servlet-mapping > para servir contenido estático
Maciek Kreft

Respuestas:

53

Se me ocurrió una solución ligeramente diferente. Es un poco hack-ish, pero aquí está el mapeo:

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

Básicamente, esto solo asigna todos los archivos de contenido por extensión al servlet predeterminado y todo lo demás a "myAppServlet".

Funciona tanto en Jetty como en Tomcat.

Taylor Gautier
fuente
13
en realidad puede agregar más de una etiqueta de patrón de URL dentro del servidor de mapeo;)
Fareed Alnamrouti
55
Servlet 2.5 y
versiones
Solo tenga cuidado con los archivos de índice (index.html) ya que pueden tener prioridad sobre su servlet.
Andres
Creo que es mala idea de uso *.sth. Si alguien obtiene la URL example.com/index.jsp?g=.sth, obtendrá la fuente del archivo jsp. ¿O estoy equivocado? (Soy nuevo en Java EE) Usualmente uso el patrón de URL /css/*y etc.
SemperPeritus
46

No es necesario una implementación completamente personalizada del servlet predeterminado en este caso, puede usar este servlet simple para ajustar la solicitud a la implementación del contenedor:


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
fuente
Esta pregunta tiene una forma ordenada de mapear / a un controlador y / estático a contenido estático usando un filtro. Verifique la respuesta votada después de la aceptada: stackoverflow.com/questions/870150/…
David Carboni
30

He tenido buenos resultados con FileServlet , ya que admite casi todo HTTP (etags, fragmentación, etc.).

Will Hartung
fuente
¡Gracias! horas de intentos fallidos y malas respuestas, y esto resolvió mi problema
Yossi Shasho
44
Aunque para servir contenido desde una carpeta fuera de la aplicación (lo uso para servir una carpeta desde el disco, digamos C: \ resources) Modifiqué esta fila: this.basePath = getServletContext (). GetRealPath (getInitParameter ("basePath ")); Y lo reemplazó con: this.basePath = getInitParameter ("basePath");
Yossi Shasho
1
Una versión actualizada está disponible en showcase.omnifaces.org/servlets/FileServlet
koppor
26

Plantilla abstracta para un servlet de recursos estáticos

En parte basado en este blog de 2007, aquí hay una plantilla abstracta modernizada y altamente reutilizable para un servlet que trata adecuadamente el almacenamiento en caché ETag, If-None-Matchy If-Modified-Since(pero no admite Gzip y Range; solo para simplificarlo; Gzip podría hacerse con un filtro o vía configuración del contenedor).

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

}

Úselo junto con la interfaz a continuación que representa un recurso estático.

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;

}

Todo lo que necesita es simplemente extender desde el servlet abstracto dado e implementar el getStaticResource()método de acuerdo con el javadoc.

Ejemplo concreto que sirve del sistema de archivos:

Aquí hay un ejemplo concreto que lo sirve a través de una URL como la /files/foo.extdel sistema de archivos del disco 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();
            }
        };
    }

}

Ejemplo concreto que sirve de la base de datos:

Aquí hay un ejemplo concreto que lo sirve a través de una URL como la /files/foo.extde la base de datos a través de una llamada de servicio EJB que devuelve a su entidad que tiene una byte[] contentpropiedad:

@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
fuente
1
Estimado @BalusC Creo que su enfoque se es vulnerable a un hacker que el envío de la petición siguiente podría navegar a través del sistema de archivos: files/%2e%2e/mysecretfile.txt. Esta solicitud produce files/../mysecretfile.txt. Lo probé en Tomcat 7.0.55. Lo llaman un directorio de escalada: owasp.org/index.php/Path_Traversal
Cristian Arteaga
1
@Cristian: Sí, posible. Actualicé el ejemplo para mostrar cómo prevenir eso.
BalusC
Esto no debería obtener votos a favor. Servir archivos estáticos para una página web con Servlet como este es una receta para la seguridad ante desastres. Todos estos problemas ya se han resuelto, y no hay ninguna razón para implementar una nueva forma personalizada con más bombas de tiempo de seguridad aún por descubrir. La ruta correcta es configurar Tomcat / GlassFish / Jetty, etc. para servir el contenido, o incluso mejor usar un servidor de archivos dedicado como NGinX.
Leonhard Printz
@LeonhardPrintz: Eliminaré la respuesta e informaré a mis amigos en Tomcat una vez que señale problemas de seguridad. No hay problema.
BalusC
19

Terminé rodando la mía StaticServlet. Es compatibleIf-Modified-Since codificación gzip y también debería ser capaz de servir archivos estáticos de archivos de guerra. No es un código muy difícil, pero tampoco es del todo trivial.

El código está disponible: StaticServlet.java . Siéntase libre de comentar.

Actualización: Khurram pregunta sobre la ServletUtilsclase a la que se hace referencia StaticServlet. Es simplemente una clase con métodos auxiliares que utilicé para mi proyecto. El único método que necesita es coalesce(que es idéntico a la función SQL COALESCE). Este es el código:

public static <T> T coalesce(T...ts) {
    for(T t: ts)
        if(t != null)
            return t;
    return null;
}
Bruno De Fraine
fuente
2
No nombre su clase interna Error. Eso puede causar confusión, ya que puede confundirlo con java.lang.Error Además, ¿es su web.xml el mismo?
Leonel
Gracias por la advertencia de error. web.xml es lo mismo, con "predeterminado" reemplazado por el nombre del StaticServlet.
Bruno De Fraine
1
En cuanto al método de fusión, se puede reemplazar (dentro de la clase Servlet) por commons-lang StringUtils.defaultString (String, String)
Mike Minicki
El método transferStreams () también se puede reemplazar con Files.copy (is, os);
Gerrit Brink
¿Por qué es tan popular este enfoque? ¿Por qué la gente está reimplementando servidores de archivos estáticos como este? Hay tantos agujeros de seguridad esperando ser descubiertos, y tantas características de servidores de archivos estáticos reales que no están implementados.
Leonhard Printz
12

A juzgar por la información de ejemplo anterior, creo que todo este artículo se basa en un comportamiento con errores en Tomcat 6.0.29 y anteriores. Ver https://issues.apache.org/bugzilla/show_bug.cgi?id=50026 . Actualice a Tomcat 6.0.30 y el comportamiento entre (Tomcat | Jetty) debería fusionarse.

Jeff Stice-Hall
fuente
1
Eso también lo entiendo svn diff -c1056763 http://svn.apache.org/repos/asf/tomcat/tc6.0.x/trunk/. ¡Por fin, después de marcar este WONTFIX hace +3 años!
Bruno De Fraine
12

prueba esto

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

Editar: esto solo es válido para la especificación del servlet 2.5 en adelante.

Alnamrouti Fareed
fuente
Parece que esta no es una configuración válida.
Gedrox
10

Tuve el mismo problema y lo resolví usando el código del 'servlet predeterminado' de la base de código de Tomcat.

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

El DefaultServlet es el servlet que sirve a los recursos estáticos (jpg, html, css, gif, etc.) en Tomcat.

Este servlet es muy eficiente y tiene algunas de las propiedades que definió anteriormente.

Creo que este código fuente es una buena manera de iniciar y eliminar la funcionalidad o las dependencias que no necesita.

  • Las referencias al paquete org.apache.naming.resources se pueden eliminar o reemplazar con el código java.io.File.
  • Las referencias al paquete org.apache.catalina.util son solo métodos / clases de utilidad que se pueden duplicar en su código fuente.
  • Las referencias a la clase org.apache.catalina.Globals se pueden insertar o eliminar.
Panagiotis Korros
fuente
Parece depender de muchas cosas de org.apache.*. ¿Cómo puedes usarlo con Jetty?
Bruno De Fraine
Tienes razón, esta versión tiene demasiadas dependencias para Tomcat (y también admite muchas cosas que quizás no quieras.
Editaré
4

Hice esto extendiendo el Tomcat DefaultServlet ( src ) y anulando el método 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);
   }
}

... Y aquí están mis asignaciones 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
fuente
1

Para atender todas las solicitudes de una aplicación Spring, así como /favicon.ico y los archivos JSP de / WEB-INF / jsp / * que solicitará AbstractUrlBasedView de Spring, puede reasignar el servlet jsp y el servlet predeterminado:

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

No podemos confiar en el patrón de url * .jsp en la asignación estándar para el servlet jsp porque el patrón de ruta '/ *' coincide antes de que se verifique cualquier asignación de extensión. La asignación del servlet jsp a una carpeta más profunda significa que coincide primero. La coincidencia '/favicon.ico' ocurre exactamente antes de la coincidencia del patrón de ruta. Las coincidencias de ruta más profundas funcionarán, o coincidencias exactas, pero ninguna coincidencia de extensión puede superar la coincidencia de ruta '/ *'. La asignación '/' al servlet predeterminado no parece funcionar. Se podría pensar que el '/' exacto superaría el patrón de ruta '/ *' en springapp.

La solución de filtro anterior no funciona para las solicitudes JSP reenviadas / incluidas de la aplicación. Para que funcione, tuve que aplicar el filtro a springapp directamente, en ese momento la coincidencia del patrón de url fue inútil ya que todas las solicitudes que van a la aplicación también van a sus filtros. Entonces agregué la coincidencia de patrones al filtro y luego aprendí sobre el servlet 'jsp' y vi que no elimina el prefijo de ruta como lo hace el servlet predeterminado. Eso resolvió mi problema, que no era exactamente el mismo, sino bastante común.


fuente
1

Comprobado para Tomcat 8.x: los recursos estáticos funcionan bien si el servlet raíz se asigna a "". Para el servlet 3.x se podría hacer por@WebServlet("")

Grigory Kislin
fuente
0

Use org.mortbay.jetty.handler.ContextHandler. No necesita componentes adicionales como StaticServlet.

En la casa del embarcadero,

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

Establezca el valor de contextPath con su prefijo de URL y establezca el valor de resourceBase como la ruta del archivo del contenido estático.

Funcionó para mi.

yogman
fuente