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_OUTPUT
- typedef std::function<void(std::string &)> receiveFunction;
- #ifdef SENDQ
- ircClient::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} {
- #else
- ircClient::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;
- }
|