/* * dCache - http://www.dcache.org/ * * Copyright (C) 2016 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 diskCacheV111.doors; import com.google.common.net.InetAddresses; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandler; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.ChannelInputShutdownEvent; import io.netty.handler.codec.LineBasedFrameDecoder; import io.netty.handler.codec.haproxy.HAProxyMessage; import io.netty.handler.codec.haproxy.HAProxyMessageDecoder; import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol; import io.netty.handler.codec.string.LineEncoder; import io.netty.handler.codec.string.LineSeparator; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.logging.LoggingHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.PrintWriter; import java.net.InetSocketAddress; import java.nio.channels.ClosedChannelException; import java.nio.charset.Charset; import java.util.Objects; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import dmg.cells.nucleus.CDC; import dmg.cells.nucleus.CellCommandListener; import dmg.cells.nucleus.CellInfoProvider; import dmg.cells.nucleus.CellMessageReceiver; import dmg.cells.nucleus.NoRouteToCellException; import dmg.util.CommandExitException; import dmg.util.LineWriter; import org.dcache.cells.AbstractCell; import org.dcache.poolmanager.PoolManagerHandler; import org.dcache.util.Args; import org.dcache.util.BoundedExecutor; import org.dcache.util.SequentialExecutor; import org.dcache.util.Transfer; /** * Login cell for line based protocols. * * <p>To be used with LoginManager. The cell is a Netty handler and expects Netty to pass * and accept Strings. These are passed on to an interpreter for processing. */ public class NettyLineBasedDoor extends AbstractCell implements ChannelInboundHandler { private static final Logger LOGGER = LoggerFactory.getLogger(NettyLineBasedDoor.class); /** * Factory for creating the protocol interpreter. */ private final NettyLineBasedInterpreterFactory factory; /** * Executor for processing commands. */ private final BoundedExecutor commandExecutor; /** * Executor for the interpreter. */ private final Executor executor; /** * Shared handler for communicating with pool manager. */ private final PoolManagerHandler poolManager; /** * Character encoding of the protocol. */ private final Charset charset; /** * Line separator encoding of the protocol. */ private final LineSeparator lineSeparator; /** * Whether to expect an HAProxy proxy protocol header. */ private final boolean expectProxyProtocol; /** * Line oriented protocol interpreter. */ private LineBasedInterpreter interpreter; /** * Cell logging context under which protocol lines are interpreted. */ private CDC cdc; /** * Netty channel. The life cycle of this cell is bound to this channel. */ private Channel channel; private String clientAddress; /** * The socket address of this host. The socket may be connected to the * client directly or to a proxy. */ private InetSocketAddress localAddress; /** * The socket address the client connected to - may be the same as {@link #localAddress} * or may be the address of some proxy service. */ private InetSocketAddress proxyAddress; /** * The address of the client's socket. May be connected directly to us * or to a proxy. */ private InetSocketAddress remoteAddress; public NettyLineBasedDoor(String cellName, Args args, NettyLineBasedInterpreterFactory factory, ExecutorService executor, PoolManagerHandler poolManagerHandler) { super(cellName, args, executor); this.factory = factory; this.executor = executor; this.commandExecutor = new SequentialExecutor(executor); this.poolManager = poolManagerHandler; this.charset = Charset.forName(args.getOption("charset", "UTF-8")); String lineSeparator = args.getOption("lineSeparator", "WINDOWS"); switch (lineSeparator) { case "WINDOWS": this.lineSeparator = LineSeparator.WINDOWS; break; case "UNIX": this.lineSeparator = LineSeparator.UNIX; break; default: throw new IllegalArgumentException("Invalid line separator value: " + lineSeparator); } this.expectProxyProtocol = args.getBooleanOption("expectProxyProtocol"); } public void messageArrived(NoRouteToCellException e) { LOGGER.warn(e.getMessage()); } protected void start(ChannelHandlerContext ctx) throws Exception { LineWriter writer = ctx::writeAndFlush; clientAddress = remoteAddress.getAddress().getHostAddress(); LOGGER.debug("Client host: {}", clientAddress); interpreter = factory.create(this, getNucleus().getThisAddress(), remoteAddress, proxyAddress, localAddress, writer, executor, poolManager); if (interpreter instanceof CellCommandListener) { addCommandListener(interpreter); } if (interpreter instanceof CellMessageReceiver) { addMessageListener((CellMessageReceiver) interpreter); } start().get(); // Blocking to prevent that we process any commands before the cell is alive } /** * Called by the cell infrastructure when the cell has been killed. * * The socket will be closed by this method. It is quite important * that this does not happen earlier, as several threads use the * output stream. This is the only place where we can be 100% * certain, that all the other threads are done with their job. * * The method blocks until the worker thread has terminated. */ @Override public void stopped() { channel.close().syncUninterruptibly(); super.stopped(); } @Override public void getInfo(PrintWriter pw) { pw.println(" User Host : " + clientAddress); LineBasedInterpreter interpreter = this.interpreter; if (interpreter instanceof CellInfoProvider) { ((CellInfoProvider) interpreter).getInfo(pw); } } protected void shutdown() { if (!commandExecutor.isShutdown()) { commandExecutor.shutdownNow(); commandExecutor.awaitTerminationUninterruptibly(); if (interpreter != null) { interpreter.shutdown(); } } kill(); } @Override public void channelRegistered(ChannelHandlerContext ctx) throws Exception { try (CDC ignored = CDC.reset(getNucleus().getThisAddress())) { Transfer.initSession(false, true); cdc = new CDC(); } channel = ctx.channel(); channel.config().setOption(ChannelOption.ALLOW_HALF_CLOSURE, true); channel.config().setOption(ChannelOption.TCP_NODELAY, true); channel.config().setOption(ChannelOption.SO_KEEPALIVE, true); } @Override public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { try (CDC ignored = cdc.restore()) { proxyAddress = localAddress = (InetSocketAddress) ctx.channel().localAddress(); remoteAddress = (InetSocketAddress) ctx.channel().remoteAddress(); if (!expectProxyProtocol) { start(ctx); } } } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { try (CDC ignored = cdc.restore()) { shutdown(); } } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof HAProxyMessage) { HAProxyMessage proxyMessage = (HAProxyMessage) msg; switch (proxyMessage.command()) { case LOCAL: ctx.close(); return; case PROXY: String sourceAddress = proxyMessage.sourceAddress(); String destinationAddress = proxyMessage.destinationAddress(); InetSocketAddress localAddress = (InetSocketAddress) ctx.channel().localAddress(); if (proxyMessage.proxiedProtocol() == HAProxyProxiedProtocol.TCP4 || proxyMessage.proxiedProtocol() == HAProxyProxiedProtocol.TCP6) { if (Objects.equals(destinationAddress, localAddress.getAddress().getHostAddress())) { /* Workaround for what looks like a bug in HAProxy - health checks should * generate a LOCAL command, but it appears they do actually use PROXY. */ ctx.close(); return; } else { this.proxyAddress = new InetSocketAddress(InetAddresses.forString(destinationAddress), proxyMessage.destinationPort()); this.remoteAddress = new InetSocketAddress(InetAddresses.forString(sourceAddress), proxyMessage.sourcePort()); } } break; } start(ctx); } else if (msg instanceof String) { if (interpreter == null) { throw new IOException("Unexpected input: " + msg); } commandExecutor.execute(new Command((String) msg)); } } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { try (CDC ignored = cdc.restore()) { if (evt instanceof ChannelInputShutdownEvent) { shutdown(); } } } @Override public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { if (cause instanceof ClosedChannelException) { LOGGER.info("Connection closed"); } else if (cause instanceof RuntimeException || cause instanceof Error) { Thread me = Thread.currentThread(); me.getUncaughtExceptionHandler().uncaughtException(me, cause); ctx.close(); } else { LOGGER.error(cause.toString()); ctx.close(); } } @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { ChannelPipeline pipeline = ctx.pipeline(); String self = ctx.name(); if (expectProxyProtocol) { pipeline.addBefore("door", "haproxy", new HAProxyMessageDecoder()); } // Decoders pipeline.addBefore(self, "frameDecoder", new LineBasedFrameDecoder(65536)); pipeline.addBefore(self, "stringDecoder", new StringDecoder(charset)); // Encoder pipeline.addBefore(self, "lineEncoder", new LineEncoder(lineSeparator, charset)); pipeline.addBefore(self, "logger", new LoggingHandler()); } @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { } private class Command implements Runnable { private final String command; public Command(String command) { this.command = command; } @Override public void run() { try (CDC ignored = cdc.restore()) { try { interpreter.execute(command); } catch (CommandExitException e) { channel.close(); } catch (RuntimeException e) { LOGGER.error("Bug detected", e); } } } } }