#include #include #include #include // #include // #include #include #include #include "config.h" #include "galaxy.h" #include "logging.h" #include "session.h" #include "utils.h" // #include Session::Session(boost::asio::ip::tcp::socket socket, boost::asio::io_service &io_service, std::string hostname, std::string port, bool server_telnet_) : server_telnet{server_telnet_}, socket_(std::move(socket)), io_service_{io_service}, resolver_{io_service}, server_{io_service}, prompt_timer_{io_service}, keep_alive_{io_service}, host{hostname}, port{port} { BUGZ_LOG(info) << "Session::Session()"; // server_sent = 0; time_ms = 50; if (CONFIG.contains("prompt_timeout")) time_ms = CONFIG["prompt_timeout"]; keepalive_secs = 45; if (CONFIG.contains("keepalive")) keepalive_secs = CONFIG["keepalive"]; // Initialize the director director.to_server = [this](const std::string &message, const std::string &source) { this->to_server(message, source); // boost::bind(&Session::to_server, this, _1, _2); }; director.to_client = boost::bind(&Session::to_client, this, _1, true); director.post = boost::bind(&Session::post, this, _1); // too soon! // director.username = rlogin_name; // replace emit_ with below: if (director.server_line) // director.server_line(s); /* emit_server_line = [this](const std::string &s) { if (director.server_line) { director.server_line(s); } }; emit_server_prompt = [this](const std::string &s) { if (director.server_prompt) { director.server_prompt(s); } }; emit_client_input ... => director.client_input */ } void Session::start(void) { BUGZ_LOG(info) << "Session::start()"; // auto self(shared_from_this()); client_read(); } Session::~Session() { BUGZ_LOG(info) << "~Session"; } /** * Returns the current server prompt. * * NOTE: This is the raw string from the server, so it can contain * color codes. Make sure you clean it before trying to test it for * any text. * * @return const std::string& */ const std::string &Session::get_prompt(void) { return server_prompt; } void Session::set_prompt(const std::string &prompt) { server_prompt = prompt; } void Session::post(notifyFunc nf) { if (nf) { BUGZ_LOG(info) << "Session::post()"; io_service_.post(nf); } else { BUGZ_LOG(error) << "Session::post( nullptr )"; } } void Session::parse_auth(void) { // how many nulls should I be seeing? // \0user\0pass\0terminal/SPEED\0 // If I don't have a proper rlogin value here, it isn't going // to work when I try to connect to the rlogin server. if (rlogin_auth.size() > 10) rlogin_name = rlogin_auth.c_str() + 1; else rlogin_name = "?"; director.username = rlogin_name; } void Session::on_connect(const boost::system::error_code error) { // We've connected to the server! WOOT WOOT! // BOOST_LOG_NAMED_SCOPE("Session"); if (!error) { BUGZ_LOG(info) << "Connected to " << host; to_client("Connected...\n\r"); connected = true; if (!server_telnet) { if (rlogin_auth[0] != 0) { // Ok, the rlogin information was junk -- to_client("Let me make up some fake rlogin data for you...\n\r"); char temp[] = "\0test\0test\0terminal/9600\0"; std::string tmp(temp, sizeof(temp)); to_server(tmp); } else { to_server(rlogin_auth); } } else { // server_telnet. Step 1: negotiate a telnet connection with TWGS. director.show_client = false; } server_read(); } else { std::string output = str(boost::format("Failed to connect: %1%:%2%\n\r") % host % port); to_client(output); BUGZ_LOG(error) << "Failed to connect to " << host << ":" << port; BUGZ_LOG(warning) << "socket.shutdown()"; socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both); } } /** * Called with the current line received from the server. * * This will do server parsing. Sector/Ports/Connecting Sectors. * Port status/inventory/%. * * See \ref split_lines() * @param line */ void Session::on_server_line(const std::string &line, const std::string &raw_line) { director.server_line(line, raw_line); } /** * Split server input into lines. * * @param line */ void Session::split_lines(std::string line) { // Does this have \n\r still on it? I don't want them. // cleanup backspaces size_t pos; while ((pos = line.find('\b')) != std::string::npos) { // backspace? OK! (unless) if (pos == 0) { // first character, so there's nothing "extra" to erase. line = line.erase(pos, 1); } else line = line.erase(pos - 1, 2); } std::string temp = clean_string(line); on_server_line(temp, line); } /* Call this with whatever we just received. That will allow me to send "just whatever I got" this time around, rather then trying to figure out what was just added to server_prompt. What about \r, \b ? Should that "reset" the server_prompt? \r should not, because it is followed by \n (eventually) and that completes my line. */ void Session::process_lines(std::string &received) { // break server_prompt into lines and send/process one by one. size_t pos, rpos; server_prompt.append(received); // I also need to break on r"\x1b[\[0-9;]*JK", treat these like \n while ((pos = server_prompt.find('\n', 0)) != std::string::npos) { std::string line; std::smatch m = ansi_newline(server_prompt); if (!m.empty()) { // We found one. size_t mpos = m.prefix().length(); // int mlen = m[0].length(); if (mpos < pos) { // Ok, the ANSI newline is before the \n // perform this process with the received line std::smatch rm = ansi_newline(received); if (!rm.empty()) { size_t rpos = rm.prefix().length(); int rlen = rm[0].length(); if (director.show_client) { line = received.substr(0, rpos + rlen); to_client(line, false); } received = rm.suffix(); } // perform this on the server_prompt line line = m.prefix(); split_lines(line); server_prompt = m.suffix(); // redo this loop -- there's still a \n in there continue; } } // process "line" in received rpos = received.find('\n', 0); // get line to send to the client if (director.show_client) { // that is, if we're sending to the client! line = received.substr(0, rpos + 1); /* std::string clean = clean_string(line); BUGZ_LOG(error) << "rpos/show_client:" << clean; */ if (server_telnet) { if (line.find('\xff') != std::string::npos) { remove_telnet_commands(line); } } to_client(line, false); } received = received.substr(rpos + 1); // process "line" in server_prompt line = server_prompt.substr(0, pos + 1); server_prompt = server_prompt.substr(pos + 1); // Remove \n for dispatching std::string part = line.substr(0, pos); /* if (server_sent != 0) { line = line.substr(server_sent); server_sent = 0; }; */ // display on? // to_client(line); // How should I handle \r in lines? For now, remove it // but LOG that we did. replace(part, "\r", ""); /* if (replace(part, "\r", "")) { BUGZ_LOG(warning) << "\\r removed from line"; } */ split_lines(part); } // Ok, we have sent all of the \n lines. if (!received.empty()) if (director.show_client) { to_client(received, false); // std::string clean = clean_string(received); // BUGZ_LOG(error) << "show_client/leftovers:" << clean; } // This is eating the entire string. String is partial line // portcim line, ending with '\r', this eats the line. /* // check the server prompt here: if ((pos = server_prompt.rfind('\r')) != std::string::npos) { // server_prompt contains \r, remove it. server_prompt = server_prompt.substr(pos + 1); } */ while ((pos = server_prompt.find('\b')) != std::string::npos) { // backspace? OK! (unless) if (pos == 0) { // first character, so there's nothing "extra" to erase. server_prompt = server_prompt.erase(pos, 1); } else server_prompt = server_prompt.erase(pos - 1, 2); } if (!server_prompt.empty()) { // We have something remaining -- start the timer! set_prompt_timer(); } } void Session::set_prompt_timer(void) { prompt_timer_.expires_after(std::chrono::milliseconds(time_ms)); prompt_timer_.async_wait(boost::bind(&Session::on_prompt_timeout, this, boost::asio::placeholders::error)); } void Session::reset_prompt_timer(void) { prompt_timer_.cancel(); } // probably no longer needed -- void Session::on_server_prompt(const std::string &prompt, const std::string &raw_prompt) { director.server_prompt(prompt, raw_prompt); if (server_telnet) { std::string ayt = std::string((const char *)"\x00\xff\xfd\xf6", 4); std::string ayt_resp = std::string((const char *)"\xff\xfb\x00", 3); std::string ayt2 = std::string((const char *)"\xff\xfb\x00", 3); std::string ayt2_resp = std::string((const char *)"\xff\xfd\x00", 3); /* for( const char & c : prompt ) { BUGZ_LOG(fatal) << "IS? " << (unsigned int)c << " " << std::hex << (unsigned int)c; } for( const char & c : ayt ) { BUGZ_LOG(fatal) << "AYT? " << (unsigned int)c << " " << std::hex << (unsigned int)c; } */ if (prompt == ayt) { to_server(ayt_resp); BUGZ_LOG(warning) << "AYT?"; server_prompt.clear(); } if (prompt == ayt2) { to_server(ayt2_resp); BUGZ_LOG(warning) << "AYT2??"; server_prompt.clear(); // let the user see what is happening... We're done negotiating (I think) director.show_client = true; } } } void Session::on_prompt_timeout(const boost::system::error_code error) { if (error != boost::asio::error::operation_aborted) { // Ok, VALID timeout if (!server_prompt.empty()) { // Here's what is happening: // SP: [ESC[2JESC[H] // which after clean_string is empty. std::string clean = server_prompt; // clean_string(server_prompt); ansi_clean(clean); if (!clean.empty()) { on_server_prompt(clean, server_prompt); } // BUGZ_LOG(trace) << "SP: [" << server_prompt << "]"; } } } void Session::server_read(void) { auto self(shared_from_this()); boost::asio::async_read( server_, boost::asio::buffer(server_buffer, sizeof(server_buffer) - 1), boost::asio::transfer_at_least(1), [this, self](boost::system::error_code ec, std::size_t length) { if (!ec) { // server_buffer[length] = 0; // server_prompt.append(server_buffer, length); std::string received(server_buffer, length); process_lines(received); /* I don't believe I need to consume this, I'm not async_reading from a stream. */ /* if (length) { // std::cout << length << std::endl; std::cout << "S: " << server_buffer << std::endl; do_write(server_buffer); } */ server_read(); } else { if (!server_prompt.empty()) { // This re-displays the very last message received (before closing) // Unfortunately, it repeats the last prompt. // (I'm looking to display the message from the server // like is full.) // BUGZ_LOG(trace) << "2C: [" << server_prompt << "]"; // to_client(server_prompt); // server_prompt.clear(); } BUGZ_LOG(warning) << "S: read_failed: socket.shutdown()"; connected = false; socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both); } }); } void Session::on_resolve( const boost::system::error_code error, const boost::asio::ip::tcp::resolver::results_type results) { // auto self(shared_from_this()); if (!error) { // Take the first endpoint. boost::asio::ip::tcp::endpoint const &endpoint = *results; server_.async_connect(endpoint, boost::bind(&Session::on_connect, this, boost::asio::placeholders::error)); } else { // TO DO: // BOOST_LOG_NAMED_SCOPE("Session"); BUGZ_LOG(error) << "Unable to resolve: " << host; std::string output = str(boost::format("Unable to resolve: %1%\n\r") % host); to_client(output); BUGZ_LOG(warning) << "socket.shutdown()"; socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both); } } void Session::client_input(const std::string &input) { std::string temp = repr(input); director.client_input(input); } void Session::client_read(void) { auto self(shared_from_this()); boost::asio::async_read( // why can't I async_read_some here? socket_, boost::asio::buffer(read_buffer, sizeof(read_buffer) - 1), boost::asio::transfer_at_least(1), [this, self](boost::system::error_code ec, std::size_t length) { if (!ec) { // read_buffer[length] = 0; if (rlogin_auth.empty()) { // first read should be rlogin information rlogin_auth.assign(read_buffer, length); // parse authentication information parse_auth(); to_client(std::string(1, 0), false); to_client("Welcome, "); to_client(rlogin_name); to_client("\n\r"); // Activate the connection to the server /* // this fails, and I'm not sure why. I've used code like this before. resolver_.async_resolve( host, port, std::bind( &Session::on_resolve, this, _1, _2)); */ // This example shows using boost::bind, which WORKS. // https://stackoverflow.com/questions/6025471/bind-resolve-handler-to-resolver-async-resolve-using-boostasio resolver_.async_resolve( host, port, boost::bind(&Session::on_resolve, this, boost::asio::placeholders::error, boost::asio::placeholders::iterator)); } else if (length) { // Proxy Active? // BOOST_LOG_NAMED_SCOPE("Session"); std::string line(read_buffer, length); client_input(line); // do_write(output); } client_read(); } else { BUGZ_LOG(warning) << "C: read_failed " << ec; if (connected) { BUGZ_LOG(warning) << "Server.shutdown()"; server_.shutdown(boost::asio::ip::tcp::socket::shutdown_both); } } }); } void Session::to_client(const std::string &message, bool log) { auto self(shared_from_this()); // output the cleaned string (so I can see what we're sending in the // logs) /* if (log) { std::string clean = clean_string(message); BUGZ_LOG(trace) << "2C: " << clean; } */ boost::asio::async_write( socket_, boost::asio::buffer(message), [this, self](boost::system::error_code ec, std::size_t /*length*/) { if (!ec) { } else { BUGZ_LOG(warning) << "2C: write failed? closed? Server.shutdown()" << ec; if (connected) { BUGZ_LOG(warning) << "Server.shutdown()"; server_.shutdown(boost::asio::ip::tcp::socket::shutdown_both); } } }); } void Session::to_server(const std::string &message) { static std::string empty; to_server(message, empty); } void Session::to_server(const std::string &message, const std::string &source) { auto self(shared_from_this()); std::string work = repr(message); if (source.empty()) BUGZ_LOG(trace) << "2S: " << work; else BUGZ_LOG(trace) << source << " 2S: " << work; boost::asio::async_write( server_, boost::asio::buffer(message), [this, self](boost::system::error_code ec, std::size_t /*length*/) { if (!ec) { } else { BUGZ_LOG(warning) << "S: write failed? closed? socket.shutdown() " << ec; // we're no longer connected. connected = false; socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both); } }); if (director.active) { start_keepin_alive(); } } void Session::start_keepin_alive(void) { // keep alive timer keep_alive_.expires_after(std::chrono::seconds(keepalive_secs)); keep_alive_.async_wait(boost::bind(&Session::stayin_alive, this, boost::asio::placeholders::error)); } void Session::stayin_alive(const boost::system::error_code error) { if (error != boost::asio::error::operation_aborted) { // stayin' alive, stayin' alive... if (director.active) { to_server(" "); BUGZ_LOG(warning) << "Session::stayin_alive()"; } } } Server::Server(boost::asio::io_service &io_service, const boost::asio::ip::tcp::endpoint &endpoint, const std::string &host, const std::string &port, bool server_telnet_) : server_telnet{server_telnet_}, io_service_{io_service}, acceptor_{io_service_, endpoint}, signal_{io_service, SIGUSR1, SIGTERM}, host_{host}, port_{port} { keep_accepting = true; BUGZ_LOG(info) << "Server::Server()"; signal_.async_wait(boost::bind(&Server::on_signal, this, boost::asio::placeholders::error, boost::asio::placeholders::signal_number)); do_accept(); } void Server::on_signal(const boost::system::error_code &ec, int signal) { BUGZ_LOG(info) << "on_signal() :" << signal; keep_accepting = false; boost::system::error_code error; acceptor_.cancel(error); BUGZ_LOG(info) << "cancel: " << error; acceptor_.close(error); BUGZ_LOG(info) << "close: " << error; } Server::~Server() { CONFIG = json(); BUGZ_LOG(info) << "Server::~Server()"; } /** * setup async connect accept * * This creates a session for each connection. Using make_shared allows the * session to automatically clean up when it is no longer active/has anything * running in the reactor. */ void Server::do_accept(void) { acceptor_.async_accept([this](boost::system::error_code ec, boost::asio::ip::tcp::socket socket) { if (!ec) { BUGZ_LOG(info) << "Server::do_accept()"; std::make_shared(std::move(socket), io_service_, host_, port_, server_telnet) ->start(); } if (keep_accepting) { BUGZ_LOG(info) << "do_accept()"; do_accept(); } }); }