/* dCache - http://www.dcache.org/ * * Copyright (C) 2015 Deutsches Elektronen-Synchrotron * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.dcache.http; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.LastHttpContent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayDeque; import java.util.Deque; /** * Add the HTTP KeepAlive related response header when appropriate and * ensure the connection is terminated once advertised. * <p> * See Section 8 of RFC-2616 for more details. */ public class KeepAliveHandler extends ChannelDuplexHandler { private static final Logger LOG = LoggerFactory.getLogger(KeepAliveHandler.class); private boolean _hasPreviousRequest; private boolean _isLastRequestKeepAlive; private final Deque<Boolean> _inflightKeepAlive = new ArrayDeque(); @Override public void channelRead(ChannelHandlerContext context, Object message) throws Exception { if (message instanceof HttpRequest) { if (_hasPreviousRequest && !_isLastRequestKeepAlive) { /* * The client has attempted to send a further request after * the previous request signaled the connection should be closed. * * For HTTP/1.1, this is plain broken: RFC 2616 (8.1.2) * "Once a close has been signaled, the client MUST NOT * send any more requests on that connection." * * For HTTP/1.0, this is undefined. * * For HTTP/1.1, dCache is required not to generate any further * responses: RFC 2616 (8.1.2.1) * "If either the client or the server sends the close * token in the Connection header, that request becomes * the last one for the connection." * * Therefore, the request is simply dropped. As soon as the * server side of the TCP connection is closed, the OS will * reply to any further traffic with a RST, tearing down the * client side of the TCP connection. */ LOG.debug("Broken client sent request after previously asking " + "the connection be closed."); return; } _isLastRequestKeepAlive = HttpHeaders.isKeepAlive((HttpRequest) message); _inflightKeepAlive.offerLast(_isLastRequestKeepAlive); _hasPreviousRequest = true; } super.channelRead(context, message); } @Override public void write(ChannelHandlerContext context, Object message, ChannelPromise promise) throws Exception { boolean is100Continue = false; if (message instanceof HttpResponse) { HttpResponse response = (HttpResponse) message; is100Continue = response.getStatus().equals(HttpResponseStatus.CONTINUE); boolean keepAlive = _inflightKeepAlive.getFirst(); /* * An upstream handler can request the connection be closed even if * the client make no such request. */ if (keepAlive && !HttpHeaders.isKeepAlive(response)) { _inflightKeepAlive.removeFirst(); _inflightKeepAlive.addFirst(Boolean.FALSE); keepAlive = false; } /* REVISIT: It is not clear from RFC-2616 whether, when the client * issues a request with both the "Expect: 100-continue" and the * "Connection: close" headers, the initial CONTINUE response and * the final response should both include the server's * "Connection: close" header, or just the initial CONTINUE, or * just the final response. * * We choose (somewhat arbitrarily) not to send the * "Connection: close" header with CONTINUE responses. */ if (!is100Continue) { HttpHeaders.setKeepAlive(response, keepAlive); } } ChannelFuture writePromise = context.write(message, promise); /* * Netty (currently) requires that the message(s) for the 100-continue * partial response contain a LastHttpContent message; therefore, an * HTTP PUT request with the "Expect: 100-continue" header will * generate two LastHttpContent messages. */ if (message instanceof LastHttpContent && !is100Continue) { boolean keepAlive = _inflightKeepAlive.remove(); if (!keepAlive) { writePromise.addListener(ChannelFutureListener.CLOSE); } } } }