/** * This file is part of Graylog. * * Graylog is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Graylog 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Graylog. If not, see <http://www.gnu.org/licenses/>. */ package org.graylog2.plugin.inputs.transports; import com.codahale.metrics.Meter; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricSet; import com.codahale.metrics.Timer; import com.google.common.collect.Maps; import com.google.common.util.concurrent.Callables; import org.graylog2.plugin.LocalMetricRegistry; import org.graylog2.plugin.MetricSets; import org.graylog2.plugin.configuration.Configuration; import org.graylog2.plugin.configuration.ConfigurationRequest; import org.graylog2.plugin.inputs.MessageInput; import org.graylog2.plugin.inputs.MisfireException; import org.graylog2.plugin.inputs.codecs.CodecAggregator; import org.graylog2.plugin.inputs.util.PacketInformationDumper; import org.graylog2.plugin.inputs.util.ThroughputCounter; import org.graylog2.plugin.journal.RawMessage; import org.jboss.netty.bootstrap.Bootstrap; import org.jboss.netty.bootstrap.ConnectionlessBootstrap; import org.jboss.netty.bootstrap.ServerBootstrap; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelHandler; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.ChannelPipeline; import org.jboss.netty.channel.ChannelPipelineFactory; import org.jboss.netty.channel.Channels; import org.jboss.netty.channel.ExceptionEvent; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.channel.SimpleChannelHandler; import org.jboss.netty.channel.SimpleChannelUpstreamHandler; import org.jboss.netty.channel.socket.DatagramChannel; import org.jboss.netty.channel.socket.DefaultDatagramChannelConfig; import org.jboss.netty.channel.socket.ServerSocketChannelConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.Callable; import static org.jboss.netty.channel.Channels.fireMessageReceived; public abstract class NettyTransport implements Transport { public static final String CK_BIND_ADDRESS = "bind_address"; public static final String CK_PORT = "port"; public static final String CK_RECV_BUFFER_SIZE = "recv_buffer_size"; private static final Logger log = LoggerFactory.getLogger(NettyTransport.class); protected final MetricRegistry localRegistry; private final InetSocketAddress socketAddress; protected final ThroughputCounter throughputCounter; private final long recvBufferSize; @Nullable private CodecAggregator aggregator; private Bootstrap bootstrap; private Channel acceptChannel; public NettyTransport(Configuration configuration, ThroughputCounter throughputCounter, LocalMetricRegistry localRegistry) { this.throughputCounter = throughputCounter; if (configuration.stringIsSet(CK_BIND_ADDRESS) && configuration.intIsSet(CK_PORT)) { this.socketAddress = new InetSocketAddress( configuration.getString(CK_BIND_ADDRESS), configuration.getInt(CK_PORT) ); } else { this.socketAddress = null; } this.recvBufferSize = configuration.intIsSet(CK_RECV_BUFFER_SIZE) ? configuration.getInt(CK_RECV_BUFFER_SIZE) : MessageInput.getDefaultRecvBufferSize(); this.localRegistry = localRegistry; localRegistry.registerAll(MetricSets.of(throughputCounter.gauges())); } private ChannelPipelineFactory getPipelineFactory(final LinkedHashMap<String, Callable<? extends ChannelHandler>> handlerList) { return new ChannelPipelineFactory() { @Override public ChannelPipeline getPipeline() throws Exception { final ChannelPipeline p = Channels.pipeline(); for (final Map.Entry<String, Callable<? extends ChannelHandler>> entry : handlerList.entrySet()) { p.addLast(entry.getKey(), entry.getValue().call()); } return p; } }; } @Override public void setMessageAggregator(@Nullable CodecAggregator aggregator) { this.aggregator = aggregator; } @Override public void launch(final MessageInput input) throws MisfireException { final LinkedHashMap<String, Callable<? extends ChannelHandler>> handlerList = getBaseChannelHandlers(input); final LinkedHashMap<String, Callable<? extends ChannelHandler>> finalHandlers = getFinalChannelHandlers(input); handlerList.putAll(finalHandlers); try { bootstrap = getBootstrap(); bootstrap.setPipelineFactory(getPipelineFactory(handlerList)); // sigh, bindable bootstraps do not share a common interface int receiveBufferSize; if (bootstrap instanceof ConnectionlessBootstrap) { acceptChannel = ((ConnectionlessBootstrap) bootstrap).bind(socketAddress); final DefaultDatagramChannelConfig channelConfig = (DefaultDatagramChannelConfig) acceptChannel.getConfig(); receiveBufferSize = channelConfig.getReceiveBufferSize(); } else if (bootstrap instanceof ServerBootstrap) { acceptChannel = ((ServerBootstrap) bootstrap).bind(socketAddress); final ServerSocketChannelConfig channelConfig = (ServerSocketChannelConfig) acceptChannel.getConfig(); receiveBufferSize = channelConfig.getReceiveBufferSize(); } else { log.error("Unknown Netty bootstrap class returned: {}. Cannot safely bind.", bootstrap); throw new IllegalStateException("Unknown netty bootstrap class returned: " + bootstrap + ". Cannot safely bind."); } if (receiveBufferSize != getRecvBufferSize()) { log.warn("receiveBufferSize (SO_RCVBUF) for input {} should be {} but is {}.", input, getRecvBufferSize(), receiveBufferSize); } } catch (Exception e) { throw new MisfireException(e); } } @Override public void stop() { if (acceptChannel != null && acceptChannel.isOpen()) { acceptChannel.close(); } if (bootstrap != null) { bootstrap.shutdown(); } } /** * Construct a {@link org.jboss.netty.bootstrap.ServerBootstrap} to use with this transport. * <p/> * Set all the options on it you need to have, but do not set a {@link org.jboss.netty.channel.ChannelPipelineFactory}, it will be replaced with the * augmented list of handlers returned by {@link #getBaseChannelHandlers(org.graylog2.plugin.inputs.MessageInput)} * * @return a configured ServerBootstrap for this transport */ protected abstract Bootstrap getBootstrap(); /** * Subclasses can override this to add additional ChannelHandlers to the pipeline to support additional features. * <p/> * Some common use cases are to add SSL/TLS, connection counters or throttling traffic shapers. * * @param input * @return the list of initial channelhandlers to add to the {@link org.jboss.netty.channel.ChannelPipelineFactory} */ protected LinkedHashMap<String, Callable<? extends ChannelHandler>> getBaseChannelHandlers(final MessageInput input) { LinkedHashMap<String, Callable<? extends ChannelHandler>> handlerList = Maps.newLinkedHashMap(); handlerList.put("exception-logger", new Callable<ChannelHandler>() { @Override public ChannelHandler call() throws Exception { return new SimpleChannelUpstreamHandler() { @SuppressWarnings("ThrowableResultOfMethodCallIgnored") @Override public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { if ("Connection reset by peer".equals(e.getCause().getMessage())) { log.trace("{} in Input [{}/{}] (channel {})", e.getCause().getMessage(), input.getName(), input.getId(), e.getChannel()); } else { log.error("Error in Input [{}/{}] (channel {})", input.getName(), input.getId(), e.getChannel(), e.getCause()); } super.exceptionCaught(ctx, e); } }; } }); handlerList.put("packet-meta-dumper", new Callable<ChannelHandler>() { @Override public ChannelHandler call() throws Exception { return new PacketInformationDumper(input); } }); handlerList.put("traffic-counter", Callables.returning(throughputCounter)); return handlerList; } /** * Subclasses can override this to modify the {@link org.jboss.netty.channel.ChannelHandler channel handlers} at the end of the pipeline. * <p/> * The default handlers in this group are the aggregation handler (e.g. for chunked GELF via UDP), which can be missing, and the {@link NettyTransport.RawMessageHandler}. * <p/> * Usually this should not be necessary, only modify them if you have a codec that cannot create a {@link org.graylog2.plugin.journal.RawMessage} for * incoming messages at the end of the pipeline. * <p/> * One valid use case would be to insert debug handlers in the middle of the list, though. * * @param input * @return the list of channel handlers at the end of the pipeline */ protected LinkedHashMap<String, Callable<? extends ChannelHandler>> getFinalChannelHandlers(final MessageInput input) { LinkedHashMap<String, Callable<? extends ChannelHandler>> handlerList = Maps.newLinkedHashMap(); if (aggregator != null) { log.debug("Adding codec aggregator {} to channel pipeline", aggregator); handlerList.put("codec-aggregator", new Callable<ChannelHandler>() { @Override public ChannelHandler call() throws Exception { return new MessageAggregationHandler(aggregator); } }); } handlerList.put("rawmessage-handler", new Callable<ChannelHandler>() { @Override public ChannelHandler call() throws Exception { return new RawMessageHandler(input); } }); return handlerList; } protected long getRecvBufferSize() { return recvBufferSize; } /** * Get the local socket address this transport is listening on after being launched. * * @return the listening address of this transport or {@code null} if the transport hasn't been launched yet. */ public SocketAddress getLocalAddress() { if (acceptChannel == null || !acceptChannel.isBound()) { return null; } return acceptChannel.getLocalAddress(); } @Override public MetricSet getMetricSet() { return localRegistry; } private class MessageAggregationHandler extends SimpleChannelHandler { private final CodecAggregator aggregator; private final Timer aggregationTimer; private final Meter invalidChunksMeter; public MessageAggregationHandler(CodecAggregator aggregator) { this.aggregator = aggregator; aggregationTimer = localRegistry.timer("aggregationTime"); invalidChunksMeter = localRegistry.meter("invalidMessages"); } @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { final Object message = e.getMessage(); if (message instanceof ChannelBuffer) { final ChannelBuffer buf = (ChannelBuffer) message; final CodecAggregator.Result result; try (Timer.Context ignored = aggregationTimer.time()) { result = aggregator.addChunk(buf); } final ChannelBuffer completeMessage = result.getMessage(); if (completeMessage != null) { log.debug("Message aggregation completion, forwarding {}", completeMessage); fireMessageReceived(ctx, completeMessage); } else if (result.isValid()) { log.debug("More chunks necessary to complete this message"); } else { invalidChunksMeter.mark(); log.debug("Message chunk was not valid and discarded."); } } else { log.debug("Could not handle netty message {}, sending further upstream.", e); fireMessageReceived(ctx, message); } } } private static class RawMessageHandler extends SimpleChannelHandler { private final MessageInput input; public RawMessageHandler(MessageInput input) { this.input = input; } @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { final Object msg = e.getMessage(); if (!(msg instanceof ChannelBuffer)) { log.error( "Invalid message type received from transport pipeline. Should be ChannelBuffer but was {}. Discarding message.", msg.getClass()); return; } final ChannelBuffer buffer = (ChannelBuffer) msg; final byte[] payload = new byte[buffer.readableBytes()]; buffer.toByteBuffer().get(payload, buffer.readerIndex(), buffer.readableBytes()); final RawMessage raw = new RawMessage(payload, (InetSocketAddress) e.getRemoteAddress()); input.processRawMessage(raw); } @Override public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { log.debug("Could not handle message, closing connection: {}", e); if (ctx.getChannel() != null && !(ctx.getChannel() instanceof DatagramChannel)) { ctx.getChannel().close(); } } } public static class Config implements Transport.Config { @Override public ConfigurationRequest getRequestedConfiguration() { final ConfigurationRequest r = new ConfigurationRequest(); r.addField(ConfigurationRequest.Templates.bindAddress(CK_BIND_ADDRESS)); r.addField(ConfigurationRequest.Templates.portNumber(CK_PORT, 5555)); r.addField(ConfigurationRequest.Templates.recvBufferSize(CK_RECV_BUFFER_SIZE, 1024 * 1024)); return r; } } }