Как много раз читать Jetty HttpInput (ServletInputStream)?

В настоящее время я разрабатываю REST API, используя RestEasy и Jetty. Один из моих планов с этим REST API — создать плагин-ловушку, чтобы делать все необходимое с входящим запросом, используя JAX-RS ContainerRequestFilter. Дело с ContainerRequestPlugin в Jetty здесь заключается в том, что после того, как я вызвал requestContext.getEntityStream(); в фильтре, мой класс EndPoint не сможет снова прочитать запрос, даже если я снова установил Entity Stream.

Ниже приведен мой код фильтра

@Provider
@Priority(2000)
public class DummyRequestFilter implements ContainerRequestFilter{
    static Logger log = Logger.getLogger(DummyRequestFilter .class.getName());
    
    @Context
    private HttpServletRequest servletRequest;
    
    @Override
    public void filter(ContainerRequestContext requestContext) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
    String requestBody = "";
    
    try {           
        IOUtils.copy(requestContext.getEntityStream(), baos);
        
        InputStream is1 = new ByteArrayInputStream(baos.toByteArray());
        InputStream is2 = new ByteArrayInputStream(baos.toByteArray());
        
        requestBody = IOUtils.toString(is1);
        
        log.info(requestBody);
        
        requestContext.setEntityStream(is2);
                
    }catch (Exception e) {
        log.log(Level.SEVERE,"Exception Occurred",e);
    }
    }   
}

Тогда вот мой класс конечной точки

@Path("/")
public class DummyService {
    
    Logger log = Logger.getLogger(DummyService .class.getName());
    
    @GET
    @Path("test")
    @Produces(MediaType.APPLICATION_JSON)
    public Response test(@FormParam("name") String name) {
        log.info("Name = "+name);

        return Response.status(200).build();
    }
}

Всякий раз, когда я вызываю этот тестовый метод, я вижу имя, отправленное в классе Filter, но имя класса Endpoint — NULL.

Позже я понял, что getEntityStream, возвращаемый из requestContext, является пользовательским ServletInputStream Jetty, то есть org.eclipse.jetty.server.HttpInput. Я считаю, что запрос не может быть прочитан в EndPoint, так как я установил Entity Stream, используя ByteArrayInputStream.

Итак, мой вопрос будет заключаться в том, есть ли способ создать/преобразовать Jetty HttpInput, используя общую реализацию InputStream? или есть другой способ обойти этот случай? где я могу много раз читать Jetty HttpInput?

С уважением


person yjatip    schedule 12.08.2020    source источник
comment
Вы можете найти некоторую помощь здесь - вам может понадобиться где-то сохранить входной поток и повторно добавить его. после прочтения, поскольку они предназначены для однократного прочтения.   -  person Gryphon    schedule 12.08.2020


Ответы (1)


Как вы, несомненно, заметили, спецификация сервлета не позволяет дважды прочитать содержимое тела запроса.

Это намеренное решение, поскольку любая такая функция потребует кэширования или буферизации содержимого тела ответа. Что приводит к:

  • Различные DoS-атаки / атаки типа «отказ в обслуживании» против вашего веб-приложения.
  • Тайм-ауты простоя при обработке запроса, когда ваш код второй раз считывает запрос из буфера и не создает сетевого трафика для сброса тайм-аута простоя.
  • Невозможность извлечь выгоду из обработки асинхронного ввода-вывода сервлетов или использовать ее.

Конечные точки JAX-RS обычно требуют, чтобы входной поток javax.servlet.http.HttpServletRequest вообще не читался по какой-либо причине (*).

Ваш код не пытается ограничить размер выделяемых вами массивов байтов, было бы легко злоупотребить вашим сервисом с помощью Почтовая бомба. (пример: отправка 42 килобайт данных, которые распаковываются до 3,99 петабайта)

Вы можете найти специфический способ реализации JAX-RS, например, использовать внутренний код Джерси для установки потока сущностей, но такой код будет ненадежным и, вероятно, приведет к необходимости исправления вашего кода и повторной компиляции с обновлениями вашей библиотеки Джерси.

Если вы идете по индивидуальному пути, будьте особенно осторожны, чтобы не создавать очевидных уязвимостей в вашем коде, ограничивать размер вашего запроса, ограничивать то, что вы можете буферизовать, и т. д.

Обычно веб-приложения, которым необходимо изменить содержимое входного потока запроса, делают это через прокси-сервлеты, которые выполняют модификацию запроса посредником в режиме реального времени, на основе буфера за буфером. У Jetty есть такой класс, который удобно называть AsyncMiddleManServlet. По сути, это означает, что ваш клиент общается с прокси-сервером, который взаимодействует с вашей конечной точкой, которая учитывает поведение сети и потребности в противодействии сети. (что-то, что фильтр буферизации не сможет правильно обработать)

(*) Вы можете случайно прочитать тело HttpServletRequest, используя вещи из запроса, которые запрашивают параметры запроса или части запроса (которые требуют, чтобы содержимое тела было прочитано для определенных конкретных типов контента)

person Joakim Erdfelt    schedule 18.08.2020
comment
Вау, большое спасибо, сэр, за подробное объяснение! Очень ценю ваш вклад. Я только что понял, что это сделано намеренно из соображений безопасности. На самом деле мне нужно регистрировать все запросы и ответы в БД, и, возможно, в будущем мне нужно будет как-то изменить тело запроса/ответа. Кстати, есть ли наилучшая практика для регистрации запросов и ответов (вставка в БД) с использованием JAX-RS и Jetty, кроме использования ContainerRequestFilter? еще раз спасибо - person yjatip; 26.08.2020
comment
Реализуйте перехватчики Jetty (ввод и вывод) для захвата тела запроса или тела ответа, вы даже можете вставлять их до/после таких вещей, как сжатие gzip, чтобы увидеть, как выглядит тело без сжатия. Таким образом, вы можете захватить необработанный поток байтов, а API сервлета даже не задействован (или знает о существовании перехватчиков). - person Joakim Erdfelt; 26.08.2020
comment
Большое спасибо, вы действительно спасли меня, сэр! ;) - person yjatip; 26.08.2020