session.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. #include <boost/bind.hpp>
  2. #include <iostream>
  3. #include <boost/format.hpp>
  4. // #include <boost/log/core.hpp>
  5. // #include <boost/log/trivial.hpp>
  6. #include <regex>
  7. #include "config.h"
  8. #include "logging.h"
  9. #include "session.h"
  10. #include <string>
  11. // #include <boost/log/attributes/named_scope.hpp>
  12. bool replace(std::string &str, const std::string &from, const std::string &to) {
  13. size_t start_pos = str.find(from);
  14. if (start_pos == std::string::npos)
  15. return false;
  16. do {
  17. str.replace(start_pos, from.length(), to);
  18. } while ((start_pos = str.find(from)) != std::string::npos);
  19. return true;
  20. }
  21. bool replace(std::string &str, const char *from, const char *to) {
  22. size_t start_pos = str.find(from);
  23. if (start_pos == std::string::npos)
  24. return false;
  25. do {
  26. str.replace(start_pos, strlen(from), to);
  27. } while ((start_pos = str.find(from)) != std::string::npos);
  28. return true;
  29. }
  30. void ansi_clean(std::string &str) {
  31. static std::regex ansi_cleaner("\x1b\[[0-9;]*[A-Zmh]",
  32. std::regex_constants::ECMAScript);
  33. str = std::regex_replace(str, ansi_cleaner, "");
  34. }
  35. void high_ascii(std::string &str) {
  36. static std::regex high_cleaner("[\x80-\xff]+",
  37. std::regex_constants::ECMAScript);
  38. str = std::regex_replace(str, high_cleaner, "#");
  39. }
  40. std::smatch ansi_newline(const std::string &str) {
  41. static std::regex ansi_nl("\x1b\[[0-9;]*[JK]",
  42. std::regex_constants::ECMAScript);
  43. std::smatch m;
  44. std::regex_search(str, m, ansi_nl);
  45. return m;
  46. }
  47. std::string clean_string(const std::string &source) {
  48. // BOOST_LOG_NAMED_SCOPE("clean_string");
  49. std::string clean = source;
  50. replace(clean, "\n", "\\n");
  51. replace(clean, "\r", "\\r");
  52. replace(clean, "\b", "\\b");
  53. // ANSI too
  54. ansi_clean(clean);
  55. // BUGZ_LOG(error) << "cleaned: " << clean;
  56. high_ascii(clean);
  57. replace(clean, "\x1b", "^");
  58. return clean;
  59. }
  60. Session::Session(boost::asio::ip::tcp::socket socket,
  61. boost::asio::io_service &io_service, std::string hostname,
  62. std::string port)
  63. : socket_(std::move(socket)), io_service_{io_service},
  64. resolver_{io_service}, server_{io_service}, timer_{io_service},
  65. keep_alive_{io_service}, host{hostname}, port{port} {
  66. // server_sent = 0;
  67. time_ms = stoi(from_config("prompt_timeout", "50"));
  68. keepalive_secs = stoi(from_config("keepalive", "45"));
  69. // director.push(new CoreDispatch((*this)));
  70. }
  71. void Session::start(void) {
  72. // BOOST_LOG_NAMED_SCOPE();
  73. // If I want the file and line number information, here's how to do it:
  74. // BUGZ_LOG(info) << boost::format("(%1%:%2%) ") % __FILE__ % __LINE__
  75. BUGZ_LOG(info) << "Session::start()";
  76. auto self(shared_from_this());
  77. // read_buffer.reserve(1024);
  78. // do_write("Welcome!\n");
  79. client_read();
  80. }
  81. Session::~Session() { BUGZ_LOG(info) << "~Session"; }
  82. /**
  83. * Returns the current server prompt.
  84. *
  85. * NOTE: This is the raw string from the server, so it can contain
  86. * color codes. Make sure you clean it before trying to test it for
  87. * any text.
  88. *
  89. * @return const std::string&
  90. */
  91. const std::string &Session::get_prompt(void) { return server_prompt; }
  92. void Session::parse_auth(void) {
  93. // how many nulls should I be seeing?
  94. // \0user\0pass\0terminal/SPEED\0
  95. // If I don't have a proper rlogin value here, it isn't going
  96. // to work when I try to connect to the rlogin server.
  97. if (rlogin_auth.size() > 10)
  98. rlogin_name = rlogin_auth.c_str() + 1;
  99. else
  100. rlogin_name = "?";
  101. }
  102. void Session::on_connect(const boost::system::error_code error) {
  103. // We've connected to the server! WOOT WOOT!
  104. // BOOST_LOG_NAMED_SCOPE("Session");
  105. if (!error) {
  106. BUGZ_LOG(info) << "Connected to " << host;
  107. to_client("Connected...\n\r");
  108. connected = true;
  109. if (rlogin_auth[0] != 0) {
  110. // Ok, the rlogin information was junk --
  111. to_client("Let me make up some fake rlogin data for you...\n\r");
  112. char temp[] = "\0test\0test\0terminal/9600\0";
  113. std::string tmp(temp, sizeof(temp));
  114. to_server(tmp);
  115. } else {
  116. to_server(rlogin_auth);
  117. }
  118. server_read();
  119. } else {
  120. // TODO:
  121. std::string output =
  122. str(boost::format("Failed to connect: %1%:%2%\n\r") % host % port);
  123. to_client(output);
  124. BUGZ_LOG(error) << "Failed to connect to " << host << ":" << port;
  125. BUGZ_LOG(warning) << "socket.shutdown()";
  126. socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
  127. }
  128. }
  129. void Session::dispatch_line(std::string line) {
  130. // Does this have \n\r still on it? I don't want them.
  131. // cleanup backspaces
  132. size_t pos;
  133. while ((pos = line.find('\b')) != std::string::npos) {
  134. // backspace? OK! (unless)
  135. if (pos == 0) {
  136. // first character, so there's nothing "extra" to erase.
  137. line = line.erase(pos, 1);
  138. } else
  139. line = line.erase(pos - 1, 2);
  140. }
  141. std::string temp = clean_string(line);
  142. // NOTE: We get "TradeWars Game Server\n" (Missing \r)
  143. // We add the \r with our injection line.
  144. if (temp.find("TradeWars Game Server ") != std::string::npos) {
  145. to_client("\rTradeWars Proxy v2++ READY (~ to activate)\n\r");
  146. // reset "active game" -- we're back at the menu
  147. }
  148. // additional analysis needed here
  149. // state : where are we, what is this line?
  150. // collect all data we can from the server. (P1)
  151. if (temp.find("Selection (? for menu): ") != std::string::npos) {
  152. char ch = temp[temp.length() - 1];
  153. if (ch >= 'A' && ch < 'Q') {
  154. BUGZ_LOG(warning) << "GAME " << ch << " activated!";
  155. }
  156. }
  157. // "Selection (? for menu): ?"
  158. // This gives us the current game that we're in.
  159. // (excluding ?, #, ! and Q)
  160. BUGZ_LOG(info) << "SL: " << temp;
  161. }
  162. /*
  163. Call this with whatever we just received.
  164. That will allow me to send "just whatever I got"
  165. this time around, rather then trying to figure out
  166. what was just added to server_prompt.
  167. What about \r, \b ? Should that "reset" the server_prompt?
  168. \r should not, because it is followed by \n (eventually)
  169. and that completes my line.
  170. */
  171. void Session::process_lines(std::string &received) {
  172. // break server_prompt into lines and send/process one by one.
  173. size_t pos, rpos;
  174. server_prompt.append(received);
  175. // I also need to break on r"\x1b[\[0-9;]*JK", treat these like \n
  176. while ((pos = server_prompt.find('\n', 0)) != std::string::npos) {
  177. std::string line;
  178. std::smatch m = ansi_newline(server_prompt);
  179. if (!m.empty()) {
  180. // We found one.
  181. size_t mpos = m.prefix().length();
  182. // int mlen = m[0].length();
  183. if (mpos < pos) {
  184. // Ok, the ANSI newline is before the \n
  185. // perform this process with the received line
  186. std::smatch rm = ansi_newline(received);
  187. if (!rm.empty()) {
  188. size_t rpos = rm.prefix().length();
  189. int rlen = rm[0].length();
  190. if (show_client) {
  191. line = received.substr(0, rpos + rlen);
  192. to_client(line);
  193. }
  194. received = rm.suffix();
  195. }
  196. // perform this on the server_prompt line
  197. line = m.prefix();
  198. dispatch_line(line);
  199. server_prompt = m.suffix();
  200. // redo this loop -- there's still a \n in there
  201. continue;
  202. }
  203. }
  204. // process "line" in received
  205. rpos = received.find('\n', 0);
  206. // get line to send to the client
  207. if (show_client) {
  208. // that is, if we're sending to the client!
  209. line = received.substr(0, rpos + 1);
  210. /*
  211. std::string clean = clean_string(line);
  212. BUGZ_LOG(error) << "rpos/show_client:" << clean;
  213. */
  214. to_client(line);
  215. }
  216. received = received.substr(rpos + 1);
  217. // process "line" in server_prompt
  218. line = server_prompt.substr(0, pos + 1);
  219. server_prompt = server_prompt.substr(pos + 1);
  220. // Remove \n for dispatching
  221. std::string part = line.substr(0, pos);
  222. /*
  223. if (server_sent != 0) {
  224. line = line.substr(server_sent);
  225. server_sent = 0;
  226. };
  227. */
  228. // display on?
  229. // to_client(line);
  230. // How should I handle \r in lines? For now, remove it
  231. // but LOG that we did.
  232. replace(part, "\r", "");
  233. /*
  234. if (replace(part, "\r", "")) {
  235. BUGZ_LOG(warning) << "\\r removed from line";
  236. }
  237. */
  238. dispatch_line(part);
  239. }
  240. // Ok, we have sent all of the \n lines.
  241. if (!received.empty())
  242. if (show_client) {
  243. to_client(received);
  244. // std::string clean = clean_string(received);
  245. // BUGZ_LOG(error) << "show_client/leftovers:" << clean;
  246. }
  247. // This is eating the entire string. String is partial line
  248. // portcim line, ending with '\r', this eats the line.
  249. /*
  250. // check the server prompt here:
  251. if ((pos = server_prompt.rfind('\r')) != std::string::npos) {
  252. // server_prompt contains \r, remove it.
  253. server_prompt = server_prompt.substr(pos + 1);
  254. }
  255. */
  256. while ((pos = server_prompt.find('\b')) != std::string::npos) {
  257. // backspace? OK! (unless)
  258. if (pos == 0) {
  259. // first character, so there's nothing "extra" to erase.
  260. server_prompt = server_prompt.erase(pos, 1);
  261. } else
  262. server_prompt = server_prompt.erase(pos - 1, 2);
  263. }
  264. if (!server_prompt.empty()) {
  265. // We have something remaining -- start the timer!
  266. set_timer();
  267. }
  268. }
  269. void Session::set_timer(void) {
  270. timer_.expires_after(std::chrono::milliseconds(time_ms));
  271. timer_.async_wait(
  272. boost::bind(&Session::on_timer, this, boost::asio::placeholders::error));
  273. }
  274. void Session::reset_timer(void) { timer_.cancel(); }
  275. void Session::on_timer(const boost::system::error_code error) {
  276. if (error != boost::asio::error::operation_aborted) {
  277. // Ok, VALID timeout
  278. if (!server_prompt.empty()) {
  279. // Here's what is happening:
  280. // SP: [ESC[2JESC[H]
  281. // which after clean_string is empty.
  282. std::string clean = clean_string(server_prompt);
  283. if (!clean.empty()) {
  284. BUGZ_LOG(warning) << "SP: [" << clean << "]";
  285. // emit
  286. }
  287. // BUGZ_LOG(trace) << "SP: [" << server_prompt << "]";
  288. }
  289. }
  290. }
  291. void Session::server_read(void) {
  292. auto self(shared_from_this());
  293. boost::asio::async_read(
  294. server_, boost::asio::buffer(server_buffer, sizeof(server_buffer) - 1),
  295. boost::asio::transfer_at_least(1),
  296. [this, self](boost::system::error_code ec, std::size_t length) {
  297. if (!ec) {
  298. server_buffer[length] = 0;
  299. // server_prompt.append(server_buffer, length);
  300. std::string received(server_buffer, length);
  301. process_lines(received);
  302. /*
  303. I don't believe I need to consume this,
  304. I'm not async_reading from a stream.
  305. */
  306. /*
  307. if (length) {
  308. // std::cout << length << std::endl;
  309. std::cout << "S: " << server_buffer << std::endl;
  310. do_write(server_buffer);
  311. }
  312. */
  313. server_read();
  314. } else {
  315. BUGZ_LOG(warning) << "S: read_failed: socket.shutdown()";
  316. socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
  317. }
  318. });
  319. }
  320. void Session::on_resolve(
  321. const boost::system::error_code error,
  322. const boost::asio::ip::tcp::resolver::results_type results) {
  323. //
  324. auto self(shared_from_this());
  325. if (!error) {
  326. // Take the first endpoint.
  327. boost::asio::ip::tcp::endpoint const &endpoint = *results;
  328. server_.async_connect(endpoint,
  329. boost::bind(&Session::on_connect, this,
  330. boost::asio::placeholders::error));
  331. } else {
  332. // TO DO:
  333. // BOOST_LOG_NAMED_SCOPE("Session");
  334. BUGZ_LOG(error) << "Unable to resolve: " << host;
  335. std::string output =
  336. str(boost::format("Unable to resolve: %1%\n\r") % host);
  337. to_client(output);
  338. BUGZ_LOG(warning) << "socket.shutdown()";
  339. socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
  340. }
  341. }
  342. void Session::client_input(const std::string &input) {
  343. if (active) {
  344. // do something amazing with the user's input.
  345. } else {
  346. if (input == "\x1b" || input == "~") {
  347. std::string prompt = clean_string(get_prompt());
  348. BUGZ_LOG(debug) << "CI: ACTIVATE prompt shows: [" << prompt << "]";
  349. }
  350. }
  351. if (talk_direct)
  352. to_server(input);
  353. BUGZ_LOG(info) << "CI: " << input;
  354. }
  355. void Session::client_read(void) {
  356. auto self(shared_from_this());
  357. boost::asio::async_read( // why can't I async_read_some here?
  358. socket_, boost::asio::buffer(read_buffer, sizeof(read_buffer) - 1),
  359. boost::asio::transfer_at_least(1),
  360. [this, self](boost::system::error_code ec, std::size_t length) {
  361. if (!ec) {
  362. read_buffer[length] = 0;
  363. if (rlogin_auth.empty()) {
  364. // first read should be rlogin information
  365. rlogin_auth.assign(read_buffer, length);
  366. // parse authentication information
  367. parse_auth();
  368. to_client(std::string(1, 0));
  369. to_client("Welcome, ");
  370. to_client(rlogin_name);
  371. to_client("\n\r");
  372. // Activate the connection to the server
  373. /* // this fails, and I'm not sure why. I've used code like this
  374. before. resolver_.async_resolve( host, port, std::bind(
  375. &Session::on_resolve, this, _1, _2)); */
  376. // This example shows using boost::bind, which WORKS.
  377. // https://stackoverflow.com/questions/6025471/bind-resolve-handler-to-resolver-async-resolve-using-boostasio
  378. resolver_.async_resolve(
  379. host, port,
  380. boost::bind(&Session::on_resolve, this,
  381. boost::asio::placeholders::error,
  382. boost::asio::placeholders::iterator));
  383. } else if (length) {
  384. // Proxy Active?
  385. // BOOST_LOG_NAMED_SCOPE("Session");
  386. std::string line(read_buffer, length);
  387. client_input(line);
  388. // do_write(output);
  389. }
  390. client_read();
  391. } else {
  392. BUGZ_LOG(warning) << "CI: read_failed";
  393. if (connected) {
  394. BUGZ_LOG(warning) << "Server.shutdown()";
  395. server_.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
  396. }
  397. }
  398. });
  399. }
  400. void Session::to_client(const std::string &message) {
  401. auto self(shared_from_this());
  402. // output the cleaned string (so I can see what we're sending in the
  403. // logs)
  404. std::string clean = clean_string(message);
  405. BUGZ_LOG(trace) << "2C: " << clean;
  406. boost::asio::async_write(
  407. socket_, boost::asio::buffer(message),
  408. [this, self](boost::system::error_code ec, std::size_t /*length*/) {
  409. if (!ec) {
  410. } else {
  411. BUGZ_LOG(warning) << "2C: write failed? closed? Server.shutdown()";
  412. if (connected) {
  413. BUGZ_LOG(warning) << "Server.shutdown()";
  414. server_.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
  415. }
  416. }
  417. });
  418. }
  419. void Session::to_server(const std::string &message) {
  420. auto self(shared_from_this());
  421. boost::asio::async_write(
  422. server_, boost::asio::buffer(message),
  423. [this, self](boost::system::error_code ec, std::size_t /*length*/) {
  424. if (!ec) {
  425. } else {
  426. BUGZ_LOG(warning) << "S: write failed? closed? socket.shutdown()";
  427. socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
  428. }
  429. });
  430. // keep alive timer
  431. keep_alive_.expires_after(std::chrono::seconds(keepalive_secs));
  432. keep_alive_.async_wait(boost::bind(&Session::stayin_alive, this,
  433. boost::asio::placeholders::error));
  434. }
  435. void Session::stayin_alive(const boost::system::error_code error) {
  436. if (error != boost::asio::error::operation_aborted) {
  437. // stayin' alive, stayin' alive...
  438. to_server(" ");
  439. BUGZ_LOG(warning) << "Session::stayin_alive()";
  440. }
  441. }
  442. Server::Server(boost::asio::io_service &io_service,
  443. const boost::asio::ip::tcp::endpoint &endpoint, std::string host,
  444. std::string port)
  445. : io_service_{io_service}, acceptor_{io_service_, endpoint}, host_{host},
  446. port_{port} {
  447. only_one = from_config("one_connection", "0") == "1";
  448. do_accept();
  449. }
  450. /**
  451. * setup async connect accept
  452. *
  453. * This creates a session for each connection. Using make_shared allows the
  454. * session to automatically clean up when it is no longer active/has anything
  455. * running in the reactor.
  456. */
  457. void Server::do_accept(void) {
  458. acceptor_.async_accept([this](boost::system::error_code ec,
  459. boost::asio::ip::tcp::socket socket) {
  460. if (!ec) {
  461. BUGZ_LOG(info) << "Server::do_accept()";
  462. std::make_shared<Session>(std::move(socket), io_service_, host_, port_)
  463. ->start();
  464. }
  465. if (!only_one)
  466. do_accept();
  467. });
  468. }
  469. /**
  470. * Clean up the trailing ../ in __FILE__
  471. *
  472. * This is used by the logging macro.
  473. *
  474. * @param filepath
  475. * @return const char*
  476. */
  477. const char *trim_path(const char *filepath) {
  478. if (strncmp(filepath, "../", 3) == 0) {
  479. filepath += 3;
  480. }
  481. return filepath;
  482. }