| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748 | #include "irc.h"#include <boost/algorithm/string.hpp>#include <iostream>void string_toupper(std::string &str) {  std::transform(str.begin(), str.end(), str.begin(), ::toupper);}/** * @brief remove channel modes (op,voice,hop,...) * * @param nick */void remove_channel_modes(std::string &nick) {  // ~&@%+  std::string remove("~&@%+");  std::string::size_type pos;  do {    pos = remove.find(nick[0]);    if (pos != std::string::npos)      nick.erase(0, 1);  } while (pos != std::string::npos);}/** * @brief split on spaces, with limit * * max is the maximum number of splits we will do. * default -1 is split all. * * "this is a test", 3  => [this][is][a test] * * @param text * @param max * @return std::vector<std::string> */std::vector<std::string> split_limit(std::string &text, int max) {  std::vector<std::string> ret;  int t = 0;  boost::split(ret, text, [&t, max](char c) {    if (c == ' ') {      ++t;      return ((max == -1) or (t < max));    };    return false;  });  return ret;}/** * @brief irc split * * If it doesn't start with a ':', split into two parts. * Otherwise 4 parts * [from] [command] [to] [message] * * @param text * @return std::vector<std::string> */std::vector<std::string> irc_split(std::string &text) {  size_t msgpos = text.find(" :");  std::string message;  if (msgpos != std::string::npos) {    message = text.substr(msgpos + 2);    text.erase(msgpos);  }  std::vector<std::string> results;  /*  if (text[0] != ':')    results = split_limit(text, 2);  results = split_limit(text, 4);  */  results = split_limit(text, -1);  if (!message.empty())    results.push_back(message);  return results;}/** * @brief parse_nick * * Parse out the nick from nick!username@host * * @param name * @return std::string */std::string parse_nick(std::string &name) {  std::string to = name;  if (to[0] == ':')    to.erase(0, 1);  size_t pos = to.find('!');  if (pos != std::string::npos) {    to.erase(pos);  }  return to;}// namespace io = boost::asio;// namespace ip = io::ip;// using tcp = boost::asio::ip; // ip::tcp;using error_code = boost::system::error_code;using namespace std::placeholders;// #define DEBUG_OUTPUTtypedef std::function<void(std::string &)> receiveFunction;#ifdef SENDQircClient::ircClient(boost::asio::io_context &io_context)    : resolver{io_context}, ssl_context{boost::asio::ssl::context::tls},      socket{io_context, ssl_context},      sendq_timer{io_context}, context{io_context} {#elseircClient::ircClient(boost::asio::io_context &io_context)    : resolver{io_context}, ssl_context{boost::asio::ssl::context::tls},      socket{io_context, ssl_context}, context{io_context} {#endif  registered = false;  nick_retry = 1;  shutdown = false;  logging = false;  channels_updated = false;  version = "Bugz IRC thing V0.1";#ifdef SENDQ  sendq_current = 0;  sendq_active = false;  sendq_ms = 500;  sendq_current = 0;#endif}std::ofstream &ircClient::log(void) {  std::time_t t = std::time(nullptr);  std::tm tm = *std::localtime(&t);  debug_file << std::put_time(&tm, "%c ");  return debug_file;}void ircClient::begin(void) {  original_nick = nick;  resolver.async_resolve(hostname, port,                         std::bind(&ircClient::on_resolve, this, _1, _2));  if (!debug_output.empty()) {    debug_file.open(debug_output.c_str(),                    std::ofstream::out | std::ofstream::app);    logging = true;  }}void ircClient::write(std::string output) {  if (logging) {    log() << "<< " << output << std::endl;  }  error_code error;  socket.write_some(boost::asio::buffer(output + "\r\n"), error);  if (error) {    if (logging) {      log() << "Write: " << error.message() << std::endl;    }  }}#ifdef SENDQ/** * @brief Add to the sendq for async slow message sending. * * target is used (we cycle through sendq_targets) so no one person can clog up * the queue. * * @param target * @param output */void ircClient::write_queue(std::string target, std::string output) {  // is target in sendq_targets  bool found = false;  for (auto &t : sendq_targets) {    if (t == target) {      found = true;      break;    }  }  if (!found) {    sendq_targets.push_back(target);  };  sendq[target].push_back(output);  if (!sendq_active) {    // async send is not active -- start the timer event    sendq_active = true;    sendq_current = 0;    sendq_timer.expires_after(std::chrono::milliseconds(sendq_ms));    sendq_timer.async_wait(std::bind(&ircClient::on_sendq, this, _1));  }}/** * @brief async send buffer * * This sends lines out slowly to the ircd.  It uses the sendq_targets to * alternate between so one person can't clog up the buffer. * * TODO:  Add something that checks to see how many lines we have sent. * After 20 lines, increase the sendq_ms (maybe 2*sendq_ms?)  After 50, maybe * 3*sendq_ms.  We want to throttle ourselves and not have the ircd doing it. * * Currently, this is not thread-safe.  It's being used by code that isn't * running the io_context in another thread. * @param error */void ircClient::on_sendq(error_code error) {  std::string &target = sendq_targets[sendq_current];  // calculate the next target  ++sendq_current;  if (sendq_current >= (int)sendq_targets.size())    sendq_current = 0;  std::string output = sendq[target].front();  sendq[target].erase(sendq[target].begin());  write(output);  if (sendq[target].size() == 0) {    // target queue is empty    sendq.erase(target);    // remove target from sendq_targets    for (auto pos = sendq_targets.begin(); pos != sendq_targets.end(); ++pos) {      // for (int x = 0; x < (int)sendq_targets.size(); ++x) {      if (*pos == target) {        sendq_targets.erase(pos);        break;      }    }    // verify the sendq_current is still valid    if (sendq_current >= (int)sendq_targets.size())      sendq_current = 0;  }  if (!sendq_targets.empty()) {    // more to do, let's do it again!    sendq_timer.expires_after(std::chrono::milliseconds(sendq_ms));    sendq_timer.async_wait(std::bind(&ircClient::on_sendq, this, _1));  } else {    // let write_queue know we aren't running anymore.    sendq_active = false;  }}#endif/** * @brief thread safe messages.push_back * * @param msg */void ircClient::message_append(message_stamp &msg) {  lock.lock();  messages.push_back(msg);  channels_updated = true;  lock.unlock();}/** * @brief thread safe message_stamp pop * * @return boost::optional<message_stamp> */boost::optional<message_stamp> ircClient::message_pop(void) {  lock.lock();  message_stamp msg;  if (messages.empty()) {    channels_updated = false;    lock.unlock();    return boost::optional<message_stamp>{};  }  msg = messages.front();  messages.erase(messages.begin());  lock.unlock();  return msg;}void ircClient::on_resolve(    error_code error, boost::asio::ip::tcp::resolver::results_type results) {  if (logging) {    log() << "Resolve: " << error.message() << std::endl;  }  if (error) {    std::string output = "Unable to resolve (DNS Issue?): " + error.message();    errors.push_back(output);    message(output);    socket.async_shutdown(std::bind(&ircClient::on_shutdown, this, _1));  }  boost::asio::async_connect(socket.next_layer(), results,                             std::bind(&ircClient::on_connect, this, _1, _2));}void ircClient::on_connect(error_code error,                           boost::asio::ip::tcp::endpoint const &endpoint) {  if (logging) {    log() << "Connect: " << error.message() << ", endpoint: " << endpoint          << std::endl;  }  if (error) {    std::string output = "Unable to connect: " + error.message();    message(output);    errors.push_back(output);    socket.async_shutdown(std::bind(&ircClient::on_shutdown, this, _1));  }  socket.async_handshake(boost::asio::ssl::stream_base::client,                         std::bind(&ircClient::on_handshake, this, _1));}void ircClient::on_handshake(error_code error) {  if (logging) {    log() << "Handshake: " << error.message() << std::endl;  }  if (error) {    std::string output = "Handshake Failure: " + error.message();    message(output);    errors.push_back(output);    socket.async_shutdown(std::bind(&ircClient::on_shutdown, this, _1));  }  write(registration());  /*  std::string request = registration();  boost::asio::async_write(socket, boost::asio::buffer(request),                           std::bind(&ircClient::on_write, this, _1, _2));  // socket.async_shutdown(std::bind(&ircClient::on_shutdown, this, _1));}void ircClient::on_write(error_code error, std::size_t bytes_transferred) {  if ((error) and (logging)) {    log() << "Write: " << error.message() << std::endl;  }  */  // << ", bytes transferred: " << bytes_transferred << "\n";  boost::asio::async_read_until(      socket, response, '\n', std::bind(&ircClient::read_until, this, _1, _2));}void ircClient::on_shutdown(error_code error) {  if (logging) {    log() << "SHUTDOWN: " << error.message() << std::endl;  }  shutdown = true;  context.stop();}void ircClient::read_until(error_code error, std::size_t bytes) {  // std::cout << "Read: " << bytes << ", " << error << "\n";  // auto data = response.data();  if (bytes == 0) {    if (logging) {      log() << "Read 0 bytes, shutdown..." << std::endl;    }    socket.async_shutdown(std::bind(&ircClient::on_shutdown, this, _1));    return;  };  // Only try to get the data -- if we're read some bytes.  auto data = response.data();  response.consume(bytes);  std::string text{(const char *)data.data(), bytes};  while ((text[text.size() - 1] == '\r') or (text[text.size() - 1] == '\n'))    text.erase(text.size() - 1);  receive(text);  // repeat until closed  boost::asio::async_read_until(      socket, response, '\n', std::bind(&ircClient::read_until, this, _1, _2));}/** * @brief Append a system message to the messages. * * @param msg */void ircClient::message(std::string msg) {  message_stamp ms;  ms.buffer.push_back(msg);  message_append(ms);}void ircClient::receive(std::string &text) {  message_stamp ms;  ms.buffer = irc_split(text);  std::vector<std::string> &parts = ms.buffer;  if (logging) {    // this also shows our parser working    std::ofstream &l = log();    l << ">> ";    for (auto &s : parts) {      l << "[" << s << "] ";    }    l << std::endl;  }  // INTERNAL IRC PARSING/TRACKING  if (parts.size() == 2) {    // hide PING / PONG messages    if (parts[0] == "PING") {      std::string output = "PONG " + parts[1];      write(output);      return;    }  }  if (parts.size() >= 3) {    std::string source = parse_nick(parts[0]);    std::string &cmd = parts[1];    std::string &msg_to = parts[2];    std::string msg;    if (parts.size() >= 4) {      msg = parts[parts.size() - 1];    }    if (logging) {      // this also shows our parser working      std::ofstream &l = log();      l << "IRC: [SRC:" << source << "] [CMD:" << cmd << "] [TO:" << msg_to        << "] [MSG:" << msg << "]" << std::endl;    }    if (cmd == "JOIN") {      channels_lock.lock();      if (nick == source) {        // yes, we are joining        std::string output = "You have joined " + msg_to;        message(output);        talkto(msg_to);        // insert empty set here.        std::set<std::string> empty;        channels[msg_to] = empty;      } else {        // Someone else is joining        std::string output = source + " has joined " += msg_to;        message(output);        channels[msg_to].insert(source);        if ((int)source.size() > max_nick_length)          max_nick_length = (int)source.size();      }      channels_lock.unlock();    }    if (cmd == "PART") {      channels_lock.lock();      if (nick == source) {        std::string output = "You left " + msg_to;        auto ch = channels.find(msg_to);        if (ch != channels.end())          channels.erase(ch);        if (!channels.empty()) {          talkto(channels.begin()->first);          // output += " [talkto = " + talkto() + "]";        } else {          talkto("");        }        // message(output);      } else {        std::string output = source + " has left " + msg_to;        if (!msg.empty()) {          output += " " + msg;        }        message(output);        channels[msg_to].erase(source);      }      find_max_nick_length();      channels_lock.unlock();    }    if (cmd == "KICK") {      std::string output =          source + " has kicked " + parts[3] + " from " + msg_to;      channels_lock.lock();      if (parts[3] == nick) {        channels.erase(msg_to);        if (!channels.empty()) {          talkto(channels.begin()->first);          output += " [talkto = " + talkto() + "]";        } else {          talkto("");        }      } else {        channels[msg_to].erase(parts[3]);      }      find_max_nick_length();      channels_lock.unlock();      message(output);    }    if (cmd == "QUIT") {      std::string output = "* " + source + " has quit ";      message(output);      channels_lock.lock();      if (source == nick) {        // We've quit?        channels.erase(channels.begin(), channels.end());      } else {        for (auto &c : channels) {          c.second.erase(source);          // would it be possible that channel is empty now?          // no, because we're still in it.        }        find_max_nick_length();      }      channels_lock.unlock();    }    if (cmd == "353") {      // NAMES list for channel      std::vector<std::string> names_list = split_limit(msg);      std::string channel = parts[4];      channels_lock.lock();      if (channels.find(channel) == channels.end()) {        // does not exist        channels.insert({channel, std::set<std::string>{}});      }      for (auto name : names_list) {        remove_channel_modes(name);        channels[channel].insert(name);      }      find_max_nick_length();      channels_lock.unlock();    }    if (cmd == "NICK") {      // msg_to.erase(0, 1);      channels_lock.lock();      for (auto &ch : channels) {        if (ch.second.erase(source) == 1) {          ch.second.insert(msg_to);        }      }      // Is this us?  If so, change our nick.      if (source == nick)        nick = msg_to;      find_max_nick_length();      channels_lock.unlock();    }    if (cmd == "PRIVMSG") {      // Possibly a CTCP request.  Let's see      std::string message = msg;      if ((message[0] == '\x01') and (message[message.size() - 1] == '\x01')) {        // CTCP handler        // NOTE:  When sent to a channel, the response is sent to the sender.        // CTCP MESSAGE FOUND  strip \x01's        message.erase(0, 1);        message.erase(message.size() - 1);        std::vector<std::string> ctcp_cmd = split_limit(message, 2);        if (ctcp_cmd[0] != "ACTION") {          std::string msg =              "Received CTCP " + ctcp_cmd[0] + " from " + parse_nick(source);          this->message(msg);          if (logging) {            log() << "CTCP : [" << message << "] from " + parse_nick(source)                  << std::endl;          }        }        if (message == "VERSION") {          std::string reply_to = parse_nick(source);          boost::format fmt = boost::format("NOTICE %1% :\x01VERSION %2%\x01") %                              reply_to % version;          std::string response = fmt.str();          write(response);          return;        }        if (message.substr(0, 5) == "PING ") {          message.erase(0, 5);          boost::format fmt = boost::format("NOTICE %1% :\x01PING %2%\x01") %                              parse_nick(source) % message;          std::string response = fmt.str();          write(response);          return;        }        if (message == "TIME") {          auto now = std::chrono::system_clock::now();          auto in_time_t = std::chrono::system_clock::to_time_t(now);          std::string datetime = boost::lexical_cast<std::string>(              std::put_time(std::localtime(&in_time_t), "%c"));          boost::format fmt = boost::format("NOTICE %1% :\x01TIME %2%\x01") %                              parse_nick(source) % datetime;          std::string response = fmt.str();          write(response);          return;        }        if (message.substr(0, 7) == "ACTION ") {          message.erase(0, 7);          parts[1] = "ACTION"; // change PRIVMSG to ACTION          parts[3] = message;        } else {          // What should I do with unknown CTCP commands?          // Unknown CTCP command.  Eat it.          return;        }      }    }  }  if (!registered) {    // We're not registered yet    if (parts[1] == "433") {      // nick collision!  Nick already in use      if (nick == original_nick) {        // try something basic        nick += "_";        std::string output = "NICK " + nick;        write(output);        return;      } else {        // Ok, go advanced        nick = original_nick + "_" + std::to_string(nick_retry);        ++nick_retry;        std::string output = "NICK " + nick;        write(output);        return;      }    }    // SASL Authentication    if ((parts[1] == "CAP") and (parts[3] == "ACK")) {      write("AUTHENTICATE PLAIN");    }    if ((parts[0] == "AUTHENTICATE") and (parts[1] == "+")) {      std::string userpass;      userpass.append(1, 0);      userpass.append(nick);      userpass.append(1, 0);      userpass.append(sasl_plain_password);      std::string asbase64 = base64encode(userpass);      std::string auth = "AUTHENTICATE " + asbase64;      write(auth);    }    if (parts[1] == "903") {      // success SASL      write("CAP END");    }    if (parts[1] == "904") {      // SASL failed      write("CAP END");      // Should we close the connection if we can't authenticate?    }    if ((parts[1] == "376") or (parts[1] == "422")) {      // END MOTD, or MOTD MISSING      find_max_nick_length(); // start with ourself.      registered = true;      if (!autojoin.empty()) {        std::string msg = "JOIN " + autojoin;        write(msg);      }    }  }  message_append(ms);}/** * @brief find max nick length * * This is for formatting the messages. * We run through the channels checking all the users, * and also checking our own nick. * * This updates \ref max_nick_length */void ircClient::find_max_nick_length(void) {  int max = 0;  for (auto const &ch : channels) {    for (auto const &nick : ch.second) {      if ((int)nick.size() > max)        max = (int)nick.size();    }  }  // check our nick against this too.  if ((int)nick.size() > max)    max = (int)nick.size();  max_nick_length = max;}std::string ircClient::registration(void) {  std::string text;  // Initiate SASL authentication  if (!sasl_plain_password.empty()) {    text = "CAP REQ :sasl\r\n";  }  if (!server_password.empty()) {    text += "PASS " + server_password + "\r\n";  }  text += "NICK " + nick + "\r\n" + "USER " + username + " 0 * :" + realname +          "\r\n";  return text;}#include <boost/archive/iterators/base64_from_binary.hpp>#include <boost/archive/iterators/transform_width.hpp>typedef boost::archive::iterators::base64_from_binary<    boost::archive::iterators::transform_width<std::string::const_iterator, 6,                                               8>>    it_base64_t;std::string base64encode(const std::string &str) {  unsigned int writePaddChars = (3 - str.length() % 3) % 3;  std::string base64(it_base64_t(str.begin()), it_base64_t(str.end()));  base64.append(writePaddChars, '=');  return base64;}
 |