/** * Copyright (C) 2014 Stratio (http://stratio.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.stratio.ingestion.source.irc; import static com.stratio.ingestion.source.irc.IRCConstants.CONF_CHANNELS; import static com.stratio.ingestion.source.irc.IRCConstants.CONF_HOST; import static com.stratio.ingestion.source.irc.IRCConstants.CONF_NAME; import static com.stratio.ingestion.source.irc.IRCConstants.CONF_NICK; import static com.stratio.ingestion.source.irc.IRCConstants.CONF_PASSWORD; import static com.stratio.ingestion.source.irc.IRCConstants.CONF_PORT; import static com.stratio.ingestion.source.irc.IRCConstants.CONF_REPLYPING; import static com.stratio.ingestion.source.irc.IRCConstants.CONF_USER; import static com.stratio.ingestion.source.irc.IRCConstants.DEFAULT_PORT; import static com.stratio.ingestion.source.irc.IRCConstants.DEFAULT_REPLYPING; import static com.stratio.ingestion.source.irc.IRCConstants.IRC_CHANNEL_PREFIX; import static com.stratio.ingestion.source.irc.IRCConstants.NAME_PREFIX; import java.io.IOException; import java.nio.charset.Charset; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; import org.apache.flume.Context; import org.apache.flume.EventDrivenSource; import org.apache.flume.channel.ChannelProcessor; import org.apache.flume.conf.Configurable; import org.apache.flume.event.EventBuilder; import org.apache.flume.instrumentation.SourceCounter; import org.apache.flume.source.AbstractSource; import org.schwering.irc.lib.IRCConnection; import org.schwering.irc.lib.IRCEventListener; import org.schwering.irc.lib.IRCModeParser; import org.schwering.irc.lib.IRCUser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; /** * * Connect IRC channels and retrieve messages from them. * * Configuration parameters are: * * <p> * <ul> * <li><tt>hostname</tt> <em>(string, required)</em>: target URI.</li> * <li><tt>port</tt> <em>(integer)</em>: Port. Default: 6667 .</li> * <li><tt>nick</tt> <em>(string, required)</em>: Nickname.</li> * <li><tt>channels</tt> <em>(string, required)</em>: Comma separated channels without hash. Example: ubuntu, * trivial. </li> * <li><tt>user</tt> <em>(string)</em>: The username. Is used to register the connection.</li> * <li><tt>name</tt> <em>(string)</em>: The realname. Is used to register the connection.</li> * <li><tt>password</tt> <em>(string)</em>: Password. Required if you are registered.</li> * <li><<tt>replyPing</tt> <em>(boolean)</em>: Automatically sends pong when receives a ping. Default: False.</li> * </ul> * </p> * */ public class IRCSource extends AbstractSource implements Configurable, EventDrivenSource { private static final Logger logger = LoggerFactory.getLogger(IRCSource.class); private IRCConnection connection = null; private String hostname; private Integer port; private String nick; private String password; private String user; private String name; private Boolean replyPing; private List<String> channels; private SourceCounter sourceCounter; class IRCConnectionListener implements IRCEventListener { Map<String, String> headers = new HashMap<String, String>(); /** * @param * @return void */ public void onRegistered() { logger.info("IRC on Registered called"); headers.put(IRCConstants.HEADER_TYPE, "registered"); send("", headers); } /** * @param * @return void */ public void onDisconnected() { logger.error("IRC source disconnected"); headers.put(IRCConstants.HEADER_TYPE, "disconnected"); send("", headers); } /** * @param msg * @return void */ public void onError(String msg) { logger.error("IRC source error: {}", msg); headers.put(IRCConstants.HEADER_TYPE, "error"); send(msg, headers); } /** * @param msg * @param num * @return void */ public void onError(int num, String msg) { logger.debug("IRC source error: {} - {}", num, msg); headers.put(IRCConstants.HEADER_TYPE, "error"); headers.put(IRCConstants.HEADER_IDENTIFIER, String.valueOf(num)); send(msg, headers); } /** * @param chan * @param user * @return void */ public void onInvite(String chan, IRCUser user, String nickPass) { logger.debug("User {} was invited to channel {}.", user.getNick(), chan); headers.put(IRCConstants.HEADER_TYPE, "invite"); headers.put(IRCConstants.HEADER_CHANNEL, chan); headers.putAll(getUser(user)); send("", headers); } /** * @param chan * @param user * @return void */ public void onJoin(String chan, IRCUser user) { logger.debug("User {} joined to channel {}.", user.getNick(), chan); headers.put(IRCConstants.HEADER_TYPE, "join"); headers.put(IRCConstants.HEADER_CHANNEL, chan); headers.putAll(getUser(user)); send("", headers); } /** * @param chan * @param user * @param nickPass * @param msg * @return void */ public void onKick(String chan, IRCUser user, String nickPass, String msg) { logger.debug("User {} was kicked from channel {}.", user.getNick(), chan); headers.put(IRCConstants.HEADER_TYPE, "kick"); headers.put(IRCConstants.HEADER_CHANNEL, chan); headers.putAll(getUser(user)); headers.put(IRCConstants.HEADER_NICKPASS, nickPass); send(msg, headers); } /** * @param nickPass * @param user * @param mode * @return void */ public void onMode(IRCUser user, String nickPass, String mode) { logger.debug("User {} changed mode.", user.getNick()); headers.put(IRCConstants.HEADER_TYPE, "mode"); headers.putAll(getUser(user)); headers.put(IRCConstants.HEADER_NICKPASS, nickPass); headers.put(IRCConstants.HEADER_MODE, mode); send("", headers); } /** * @param chan * @param user * @param mp * @return void */ public void onMode(String chan, IRCUser user, IRCModeParser mp) { logger.debug("User {} changed {} channel mode.", user.getNick(), chan); headers.put(IRCConstants.HEADER_TYPE, "mode"); headers.put(IRCConstants.HEADER_CHANNEL, chan); headers.putAll(getUser(user)); headers.put(IRCConstants.HEADER_IRCMODEPARSER, mp.toString()); send("", headers); } /** * @param nickNew * @param user * @return void */ public void onNick(IRCUser user, String nickNew) { logger.debug("User {} changed his nick to {} channel mode.", user.getNick(), nickNew); headers.put(IRCConstants.HEADER_TYPE, "nick"); headers.putAll(getUser(user)); headers.put(IRCConstants.HEADER_NEWNICK, user.getNick()); send("", headers); } /** * @param target * @param user * @param msg * @return void */ public void onNotice(String target, IRCUser user, String msg) { logger.debug("User {} noticed {} to {}.", user.getNick(), msg, target); headers.put(IRCConstants.HEADER_TYPE, "notice"); headers.putAll(getUser(user)); headers.put(IRCConstants.HEADER_TARGET, target); send(msg, headers); } /** * @param chan * @param user * @param msg * @return void */ public void onPart(String chan, IRCUser user, String msg) { logger.debug("User {} is part of channel {}.", user.getNick(), chan); headers.put(IRCConstants.HEADER_TYPE, "part"); headers.put(IRCConstants.HEADER_CHANNEL, chan); headers.putAll(getUser(user)); send(msg, headers); } /** * @param chan * @param user * @param msg * @return void */ public void onPrivmsg(String chan, IRCUser user, String msg) { logger.debug("User {} wrote a message to {} - {}", user.getNick(), chan, msg); headers.put(IRCConstants.HEADER_TYPE, "privmsg"); headers.put(IRCConstants.HEADER_CHANNEL, chan); headers.putAll(getUser(user)); send(msg, headers); } /** * @param msg * @param user * @return void */ public void onQuit(IRCUser user, String msg) { logger.debug("User {} left the chat."); headers.put(IRCConstants.HEADER_TYPE, "quit"); headers.putAll(getUser(user)); send(msg, headers); } /** * @param msg * @param num * @param value * @return void */ public void onReply(int num, String value, String msg) { logger.debug("A numeric reply with identifier {} and value {} was received.", num, value); headers.put(IRCConstants.HEADER_TYPE, "reply"); send(msg, headers); } /** * @param chan * @param user * @param topic * @return void */ public void onTopic(String chan, IRCUser user, String topic) { logger.debug("User {} changed {} channel topic to {}", user.getNick(), chan, topic); headers.put(IRCConstants.HEADER_TYPE, "topic"); headers.putAll(getUser(user)); headers.put(IRCConstants.HEADER_CHANNEL, chan); headers.put(IRCConstants.HEADER_TOPIC, topic); send(topic, headers); } /** * @param ping * @return void */ public void onPing(String ping) { logger.debug("Ping {}." + ping); headers.put(IRCConstants.HEADER_TYPE, "ping"); headers.put(IRCConstants.HEADER_TYPE, "unknown"); send(ping, headers); if(replyPing){ connection.doPong(ping); } } /** * @param prefix * @param command * @param middle * @param trailing * @return void */ public void unknown(String prefix, String command, String middle, String trailing) { logger.debug("Unknown event was received. "); headers.put(IRCConstants.HEADER_TYPE, "unknown"); headers.put(IRCConstants.HEADER_TRAILING, trailing); headers.put(IRCConstants.HEADER_PREFIX, middle); headers.put(IRCConstants.HEADER_COMMAND, command); headers.put(IRCConstants.HEADER_MIDDLE, middle); send("", headers); } /** * @param user * @return void */ private Map<String, String> getUser(IRCUser user) { Map<String, String> usermap = new HashMap(); usermap.put(IRCConstants.HEADER_USERNAME, user.getUsername()); usermap.put(IRCConstants.HEADER_SERVERNAME, user.getServername()); usermap.put(IRCConstants.HEADER_NICK, user.getNick()); usermap.put(IRCConstants.HEADER_HOST, user.getHost()); return usermap; } /** * @param message * @param headers * @return void */ private void send(String message, Map<String, String> headers) { ChannelProcessor channelProcessor = getChannelProcessor(); sourceCounter.addToEventReceivedCount(1); sourceCounter.incrementAppendBatchReceivedCount(); channelProcessor.processEvent(EventBuilder.withBody(message, Charset.forName("UTF-8"), headers)); sourceCounter.addToEventAcceptedCount(1); sourceCounter.incrementAppendBatchAcceptedCount(); headers.clear(); } } @Override public void configure(Context context) { hostname = context.getString(CONF_HOST); port = context.getInteger(CONF_PORT, DEFAULT_PORT); nick = context.getString(CONF_NICK); channels = Arrays.asList(context.getString(CONF_CHANNELS).split(",")); password = context.getString(CONF_PASSWORD); user = context.getString(CONF_USER, NAME_PREFIX + new Random().nextInt(Integer.MAX_VALUE)); name = context.getString(CONF_NAME, user); replyPing = context.getBoolean(CONF_REPLYPING, DEFAULT_REPLYPING); Preconditions.checkState(hostname != null, "No hostname specified"); Preconditions.checkState(nick != null, "No nick specified"); Preconditions.checkState(!channels.isEmpty(), "No channels specified"); if (sourceCounter == null) { sourceCounter = new SourceCounter(getName()); } } @Override public synchronized void start() { logger.info("IRC source starting"); try { createConnection(); } catch (Exception e) { logger.error("Unable to create irc client using hostname:" + hostname + " port:" + port ); destroyConnection(); return; } super.start(); } @Override public synchronized void stop() { logger.info("IRC source {} stopping", this.getName()); destroyConnection(); super.stop(); logger.debug("IRC source {} stopped.", this.getName()); } private void createConnection() throws IOException, InterruptedException { if (connection == null) { logger.debug( "Creating new connection to hostname:{} port:{}", hostname, port); connection = new IRCConnection(hostname, new int[] { port }, password, nick, user, name); connection.addIRCEventListener(new IRCConnectionListener()); connection.setEncoding("UTF-8"); connection.setPong(true); connection.setDaemon(false); connection.setColors(false); connection.connect(); for (String chan : channels) { connection.doJoin(IRC_CHANNEL_PREFIX + chan); } } } private void destroyConnection() { if (connection != null) { logger.debug("Destroying connection to: {}:{}", hostname, port); connection.close(); } connection = null; } @VisibleForTesting protected SourceCounter getSourceCounter() { return sourceCounter; } }