Servlet para veicular conteúdo estático

145

Eu implanto um aplicativo da web em dois contêineres diferentes (Tomcat e Jetty), mas seus servlets padrão para veicular o conteúdo estático têm uma maneira diferente de lidar com a estrutura de URL que eu quero usar ( detalhes ).

Portanto, pretendo incluir um pequeno servlet no aplicativo da web para exibir seu próprio conteúdo estático (imagens, CSS, etc.). O servlet deve ter as seguintes propriedades:

  • Sem dependências externas
  • Simples e confiável
  • Suporte para If-Modified-Sincecabeçalho (isto é, getLastModifiedmétodo personalizado )
  • (Opcional) suporte para codificação gzip, etags, ...

Esse servlet está disponível em algum lugar? O mais próximo que posso encontrar é o exemplo 4-10 do livro de servlets.

Atualização: A estrutura da URL que eu quero usar - caso você esteja se perguntando - é simplesmente:

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

Portanto, todas as solicitações devem ser passadas para o servlet principal, a menos que sejam para o staticcaminho. O problema é que o servlet padrão do Tomcat não leva em conta o ServletPath (ele procura os arquivos estáticos na pasta principal), enquanto o Jetty o faz (o mesmo ocorre na staticpasta).

Bruno De Fraine
fonte
Você poderia elaborar a "estrutura de URL" que deseja usar? Rolar o seu próprio, com base no exemplo 4-10 vinculado, parece um esforço trivial. Eu já fiz isso muitas vezes ...
Stu Thompson
Editei minha pergunta para elaborar a estrutura da URL. E sim, acabei lançando meu próprio servlet. Veja minha resposta abaixo.
Bruno De Fraine
1
Por que você não usa o servidor da web para conteúdo estático?
Stephen
4
@ Stephen: porque nem sempre existe um Apache na frente do Tomcat / Jetty. E para evitar o incômodo de uma configuração separada. Mas você está certo, eu poderia considerar essa opção.
Bruno De Fraine
Eu simplesmente não consigo entender, por que você não usou o mapeamento como este <servlet-mapping> <servlet-name> padrão </servlet-name> <url-pattern> / </url-pattern> </ servlet-mapping > para veicular conteúdo estático
Maciek Kreft

Respostas:

53

Eu vim com uma solução um pouco diferente. É um pouco hack-ish, mas aqui está o mapeamento:

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

Isso basicamente mapeia todos os arquivos de conteúdo por extensão para o servlet padrão e todo o resto para "myAppServlet".

Funciona no Jetty e no Tomcat.

Taylor Gautier
fonte
13
na verdade, você pode adicionar mais de uma tag de padrão de url dentro do mapeamento servelet;)
Fareed Alnamrouti
5
Servlet 2.5 e mais recente suportam várias tags de padrão de URL dentro do mapeamento de servlet
vivid_voidgroup
Apenas tome cuidado com os arquivos de índice (index.html), pois eles podem ter precedência sobre seu servlet.
Andres
Eu acho que é má ideia usar *.sth. Se alguém receber url, example.com/index.jsp?g=.sthele receberá a fonte do arquivo jsp. Ou eu estou errado? (Eu sou novo no Java EE) Eu costumo usar padrão de URL /css/*e etc.
SemperPeritus
46

Não há necessidade de implementação completamente customizada do servlet padrão. Nesse caso, você pode usar este servlet simples para agrupar a solicitação na implementação do contêiner:


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
fonte
Esta pergunta tem uma maneira clara de mapear / para um controlador e / estática para conteúdo estático usando um filtro. Verifique a resposta votada após a aceita: stackoverflow.com/questions/870150/…
David Carboni
30

Eu tive bons resultados com o FileServlet , pois ele suporta praticamente todo o HTTP (etags, chunking etc.).

Will Hartung
fonte
Obrigado! horas de tentativas fracassadas e respostas ruins, e isso resolveu o meu problema
Yossi Shasho
4
Embora, para veicular o conteúdo de uma pasta fora do aplicativo (eu o uso para servidor de uma pasta do disco, digamos C: \ resources), modifiquei esta linha: this.basePath = getServletContext (). GetRealPath (getInitParameter ("basePath ")); E substituiu-o por: this.basePath = getInitParameter ("basePath");
Yossi Shasho
1
Uma versão atualizada está disponível em showcase.omnifaces.org/servlets/FileServlet
koppor
26

Modelo abstrato para um servlet de recurso estático

Parcialmente baseado neste blog a partir de 2007, aqui está um modelo abstrato modernizado e altamente reutilizável para um servlet que corretamente lida com armazenamento em cache, ETag, If-None-Matche If-Modified-Since(mas nenhum apoio Gzip e Gama, basta mantê-lo simples; Gzip poderia ser feito com um filtro ou via configuração de contêiner).

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

}

Use-o junto com a interface abaixo que representa um 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;

}

Tudo o que você precisa é apenas estender-se a partir do servlet abstrato fornecido e implementar o getStaticResource() método de acordo com o javadoc.

Exemplo concreto que serve a partir do sistema de arquivos:

Aqui está um exemplo concreto que o serve por meio de uma URL, como /files/foo.extno sistema de arquivos em 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();
            }
        };
    }

}

Exemplo concreto que serve do banco de dados:

Aqui está um exemplo concreto que o serve por meio de uma URL, como /files/foo.extno banco de dados, por meio de uma chamada de serviço EJB, que retorna sua entidade com uma byte[] contentpropriedade:

@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
fonte
1
Caro @BalusC Eu acho que sua abordagem é é vulnerável a um hacker que enviar o seguinte pedido pode navegar através do sistema de arquivos: files/%2e%2e/mysecretfile.txt. Essa solicitação produz files/../mysecretfile.txt. Eu testei no Tomcat 7.0.55. Eles chamam de escalada de diretório: owasp.org/index.php/Path_Traversal
Cristian Arteaga
1
@ Cristian: Sim, possível. Atualizei o exemplo para mostrar como evitar isso.
precisa saber é o seguinte
Isso não deve receber votos positivos. Servir arquivos estáticos para uma página da Web com o Servlet como esta é uma receita para a segurança de desastres. Todos esses problemas já foram resolvidos e não há razão para implementar uma nova maneira personalizada, com provavelmente bombas-relógio de segurança ainda não descobertas. O caminho correto é configurar o Tomcat / GlassFish / Jetty etc para servir o conteúdo, ou melhor ainda, usar um servidor de arquivos dedicado como o NGinX.
Leonhard Printz
@LeonhardPrintz: vou excluir a resposta e informar os meus amigos no Tomcat quando você apontar problemas de segurança. Sem problemas.
BalusC 23/03
19

Acabei rolando sozinho StaticServlet. SuportaIf-Modified-Since codificação gzip e deve poder servir arquivos estáticos de arquivos war também. Não é um código muito difícil, mas também não é totalmente trivial.

O código está disponível: StaticServlet.java . Sinta-se livre para comentar.

Atualização: Khurram pergunta sobre a ServletUtilsclasse em que é referenciada StaticServlet. É simplesmente uma classe com métodos auxiliares que eu usei para o meu projeto. O único método que você precisa é coalesce(que é idêntico à função SQL COALESCE). Este é o código:

public static <T> T coalesce(T...ts) {
    for(T t: ts)
        if(t != null)
            return t;
    return null;
}
Bruno De Fraine
fonte
2
Não nomeie seu erro de classe interna. Isso pode causar confusão, pois você pode confundi-lo com java.lang.Error Além disso, seu web.xml é o mesmo?
Leonel
Obrigado pelo aviso de erro. web.xml é o mesmo, com "padrão" substituído pelo nome do StaticServlet.
Bruno De Fraine
1
Quanto ao método de coalescem, que pode ser substituído (dentro da classe servlet) por Commons-Lang StringUtils.defaultString (String, String)
Mike Minicki
O método transferStreams () também pode ser substituído por Files.copy (is, os);
Gerrit Brink
Por que essa abordagem é tão popular? Por que as pessoas estão reimplementando servidores de arquivos estáticos como este? Existem tantas falhas de segurança aguardando para serem descobertas e muitos recursos de servidores de arquivos estáticos reais que não são implementados.
Leonhard Printz 23/03
12

A julgar pelas informações de exemplo acima, acho que este artigo inteiro se baseia em um comportamento com erros no Tomcat 6.0.29 e versões anteriores. Consulte https://issues.apache.org/bugzilla/show_bug.cgi?id=50026 . Atualize para o Tomcat 6.0.30 e o comportamento entre (Tomcat | Jetty) deve ser mesclado.

Jeff Stice-Hall
fonte
1
Essa é também a minha compreensão svn diff -c1056763 http://svn.apache.org/repos/asf/tomcat/tc6.0.x/trunk/. Finalmente, depois de marcar este WONTFIX + 3 anos atrás!
Bruno De Fraine
12

tente isso

<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: Isso é válido apenas para as especificações do servlet 2.5 e posterior.

Alnamrouti com tarifa
fonte
Parece que esta não é uma configuração válida.
Gedrox
10

Eu tive o mesmo problema e o resolvi usando o código do 'servlet padrão' da base de código do Tomcat.

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

O DefaultServlet é o servlet que serve os recursos estáticos (jpg, html, css, gif etc.) no Tomcat.

Este servlet é muito eficiente e possui algumas das propriedades definidas acima.

Eu acho que esse código fonte é uma boa maneira de iniciar e remover as funcionalidades ou impedimentos que você não precisa.

  • As referências ao pacote org.apache.naming.resources podem ser removidas ou substituídas pelo código java.io.File.
  • As referências ao pacote org.apache.catalina.util são propensamente apenas métodos / classes utilitários que podem ser duplicados no seu código-fonte.
  • As referências à classe org.apache.catalina.Globals podem ser embutidas ou removidas.
Panagiotis Korros
fonte
Parece depender de muitas coisas org.apache.*. Como você pode usá-lo com o Jetty?
Bruno De Fraine
Você está certo, esta versão tem muitas depedencies para o Tomcat (caand ele também suporta muitas coisas que você pode não querer vou editar a minha resposta..
Panagiotis Korros
4

Eu fiz isso estendendo o tomcat DefaultServlet ( src ) e substituindo o 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);
   }
}

... E aqui estão meus mapeamentos 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
fonte
1

Para atender a todas as solicitações de um aplicativo Spring, bem como /favicon.ico e os arquivos JSP de / WEB-INF / jsp / * que o AbstractUrlBasedView do Spring solicitará, basta remapear o servlet jsp e o servlet padrão:

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

Não podemos confiar no padrão de URL * .jsp no mapeamento padrão para o servlet jsp porque o padrão de caminho '/ *' é correspondido antes de qualquer mapeamento de extensão ser verificado. Mapear o servlet jsp para uma pasta mais profunda significa que ele corresponde primeiro. A correspondência '/favicon.ico' acontece exatamente antes da correspondência do padrão de caminho. As correspondências mais profundas do caminho funcionarão ou correspondências exatas, mas nenhuma correspondência de extensão poderá ultrapassar a correspondência do caminho '/ *'. Mapear '/' para o servlet padrão não parece funcionar. Você pensaria que o '/' exato venceria o padrão de caminho '/ *' no springapp.

A solução de filtro acima não funciona para solicitações JSP encaminhadas / incluídas do aplicativo. Para fazê-lo funcionar, tive que aplicar o filtro diretamente ao springapp, momento em que a correspondência de padrões de URL era inútil, pois todas as solicitações que vão para o aplicativo também vão para seus filtros. Então, adicionei a correspondência de padrões ao filtro e aprendi sobre o servlet 'jsp' e vi que ele não remove o prefixo do caminho, como o servlet padrão. Isso resolveu meu problema, que não era exatamente o mesmo, mas era bastante comum.


fonte
1

Verificado para o Tomcat 8.x: os recursos estáticos funcionam bem se o servlet raiz mapear para "". Para o servlet 3.x, isso pode ser feito por@WebServlet("")

Grigory Kislin
fonte
0

Use org.mortbay.jetty.handler.ContextHandler. Você não precisa de componentes adicionais como StaticServlet.

Na casa do cais,

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>

Defina o valor de contextPath com seu prefixo de URL e defina o valor de resourceBase como o caminho do arquivo do conteúdo estático.

Funcionou para mim.

yogman
fonte