#include "terminal.h" #include "utils.h" #include "wordplay.h" #include #include #include #include #include #include #include #include #include // random() #include #include #include // strcasecmp #include #include #include #include #include struct console_details console; /* https://softwareengineering.stackexchange.com/questions/141973/how-do-you-achieve-a-numeric-versioning-scheme-with-git Use tags to mark commits with version numbers: git tag -a v2.5 -m 'Version 2.5' Push tags upstream—this is not done by default: git push --tags Then use the describe command: git describe --tags --long */ std::string version = HHVERSION; /* We get the username/fullname from tailing the node logs, and reading the user.dat file. */ std::string username; std::string fullname; // #include // handle Ctrl-C/SIGINT /* Log level guideline: * - ZF_LOG_FATAL - happened something impossible and absolutely unexpected. * Process can't continue and must be terminated. * Example: division by zero, unexpected modifications from other thread. * - ZF_LOG_ERROR - happened something possible, but highly unexpected. The * process is able to recover and continue execution. * Example: out of memory (could also be FATAL if not handled properly). * - ZF_LOG_WARN - happened something that *usually* should not happen and * significantly changes application behavior for some period of time. * Example: configuration file not found, auth error. * - ZF_LOG_INFO - happened significant life cycle event or major state * transition. * Example: app started, user logged in. * - ZF_LOG_DEBUG - minimal set of events that could help to reconstruct the * execution path. Usually disabled in release builds. * - ZF_LOG_VERBOSE - all other events. Usually disabled in release builds. * * *Ideally*, log file of debugged, well tested, production ready application * should be empty or very small. Choosing a right log level is as important as * providing short and self descriptive log message. */ /* #define ZF_LOG_VERBOSE 1 #define ZF_LOG_DEBUG 2 #define ZF_LOG_INFO 3 #define ZF_LOG_WARN 4 #define ZF_LOG_ERROR 5 #define ZF_LOG_FATAL 6 */ // When debugging low-level, use this: // #define ZF_LOG_LEVEL ZF_LOG_VERBOSE // Except this doesn't work. It needs to be anywere the // zf_log.h is included. // LOGGING with file output #include "zf_log.h" FILE *g_log_file; static void file_output_callback(const zf_log_message *msg, void *arg) { (void)arg; *msg->p = '\n'; fwrite(msg->buf, msg->p - msg->buf + 1, 1, g_log_file); fflush(g_log_file); } static void file_output_close(void) { fclose(g_log_file); } static int file_output_open(const char *const log_path) { g_log_file = fopen(log_path, "a"); if (!g_log_file) { ZF_LOGW("Failed to open log file %s", log_path); return 0; } atexit(file_output_close); zf_log_set_output_v(ZF_LOG_PUT_STD, 0, file_output_callback); return 1; } void log_flush(void) { fflush(g_log_file); } // END LOGGING /* What is the name of the actual, real Mystic executable that we'll be executing and mangling? */ #define TARGET "./mySTIC" // Size of our input and output buffers. #define BSIZE 1024 /* These are harry "timeout" events. These happen when we've been sitting around awhile. */ int node; /* This only works for those few idiots that use the horribly broken SSH crap that Mystic uses. */ int locate_user(const char *alias) { FILE *user; char buffer[0x600]; char temp[100]; user = fopen("data/users.dat", "rb"); if (user == NULL) return 0; // Carry on! while (fread(buffer, 0x600, 1, user) == 1) { pcopy(buffer + 0x6d, temp); if (strcasecmp(temp, username.c_str()) == 0) { pcopy(buffer + 0x8c, temp); // Assuming the user record isn't screwed up with len=0. fullname.assign(temp); fclose(user); return 1; // break; } /* printf("Alias: %s\n", temp); pcopy(buffer + 0x8c, temp ); printf("Full Name: %s\n", temp ); */ } fclose(user); return 0; } std::ifstream logfile; std::streampos log_pos; void open_mystic_log(void) { std::string mystic_logfile; { std::ostringstream buffer; buffer << "logs/node" << node << ".log"; mystic_logfile = buffer.str(); }; logfile.open(mystic_logfile, std::ios_base::in | std::ios_base::ate); // Ok, we're at the end of the file. Or should be. if (logfile.is_open()) { ZF_LOGD("Log %s open", (const char *)mystic_logfile.c_str()); log_pos = logfile.tellg(); } else { ZF_LOGE("Failed to open: %s", (const char *)mystic_logfile.c_str()); } } void scan_mystic_log(void) { if (logfile.is_open()) { int again = 0; do { std::string line = find_new_text(logfile, log_pos); if (line.empty()) return; again = 1; ZF_LOGD("mystic log: %s", (const char *)line.c_str()); // Ok, we have a line, look for interesting details if (line.find("New user application") != std::string::npos) { ZF_LOGE("New User"); } size_t pos; pos = line.find("Created Account: "); if (pos != std::string::npos) { pos += 18 - 1; // Ok, find the end '#' size_t len = line.find('#', pos); if (len != std::string::npos) { username = line.substr(pos, len - pos - 1); ZF_LOGE("New User: %s", (const char *)username.c_str()); // once we know this works -- lookup user's record locate_user(username.c_str()); ZF_LOGE("Username: [%s] A.K.A. [%s]", (const char *)username.c_str(), (const char *)fullname.c_str()); } } pos = line.find(" logged in"); if (pos != std::string::npos) { --pos; size_t len = 20; // position 20 is right after date/time username = line.substr(20, pos + 1 - len); ZF_LOGE("User: %s", (const char *)username.c_str()); // verify this works, lookup locate_user(username.c_str()); ZF_LOGE("Username: [%s] A.K.A. [%s]", (const char *)username.c_str(), (const char *)fullname.c_str()); } } while (again); } } /* This is done. :D My buffering system works with stack'em. TO FIX: Stop using c strings, must use char * buffer + int length. MAY CONTAIN NULL VALUES. Rework some things here. Here's the "plan": if buffer is EMPTY: time_idle = 1; // setup for "random timeout value mess" // we're in luck! The last parameter is time interval/timeout. :D timeout.tv_sec = 10; // randrange(10-25) timeout.tv_usec = 0; NOT EMPTY: // we're in luck! The last parameter is time interval/timeout. :D timeout.tv_sec = 0; timeout.tv_usec = 10; // Wild Guess Here? Maybe higher, maybe lower? time_idle = 0; ON READ: read/append to current buffer. We can't use nulls -- what if they are using ZModem, there's nulls in the file! Look for trailing / the very last "\r\n". (I could mangle/chunk it line by line. But I'm not sure I'd need to do that.) Optional "mangle" buffer up to that very point -- and send up to that point. Option #2: Maybe we send everything if program has been running for under 20 seconds. This would allow the ANSI detect to not get screwed up by this new idea. ON TIMEOUT: if time_idle: Activate funny harry timeout events. else: Ok, we *STILL* haven't received any more characters into the buffer -- even after waiting. (Maybe we haven't waited long enough?) send the pending information in the buffer and clear it out. Maybe this is a prompt, and there won't be a \r\n. This allows for cleaner process of "lines" of buffer. We shouldn't break in the midDLE OF A WORD. Downside is that we sit on buffer contents a little while / some amount of time -- which will add some lag to prompts showing up. (LAG? Are you kidding?) ZModem: start: "rz^M**"... 05-12 18:12:15.916 >> rz^M**^XB00000000000000^M<8A>^Q 05-12 18:12:15.928 << **\x18B0100000023be50\r\n\x11 05-12 18:12:15.928 >> *^XC^D 05-12 18:12:15.939 << **\x18B0900000000a87c\r\n\x11 05-12 18:12:15.940 >> *^XC # Start of PK zipfile. 05-12 18:12:15.941 >> PK^C^D^T end: 05-12 18:26:38.700 << **\x18B0100000023be50\r\n\x11 05-12 18:26:38.700 >> **^XB0823a77600344c^M<8A> 05-12 18:26:38.711 << **\x18B0800000000022d\r\n 05-12 18:26:38.712 >> OO^MESC[0m */ // TODO: Get everything above this -- into another file. int main(int argc, char *argv[]) { int master; pid_t pid; node = -1; tzset(); init_harry(); srandom(time(NULL)); // ./mystic -TID7 -IP192.168.0.1 -HOSTUnknown -ML1 -SL0 -ST2 -CUnknown // -Ubugz -PUWISHPASSWORD // ./mystic -TID7 -IP192.168.0.1 -HOSTUnknown -ML0 -SL0 -ST0 -CUnknown // ./mystic -TID7 -IP192.168.0.1 -HOSTUnknown -ML1 -SL0 -ST2 -CUnknown // -Ubugz -PUWISH // ./mystic -TID7 -IP192.168.0.1 -HOSTUnknown -ML0 -SL0 -ST0 -CUnknown // ./mystic -TID7 -IP192.168.0.1 -HOSTUnknown -ML0 -SL0 -ST0 -CUnknown // ./mystic -TID9 -IP192.168.0.1 -HOSTUnknown -ML0 -SL1 -ST0 -CUnknown // ./mystic -TID7 -IP192.168.0.1 -HOSTUnknown -ML1 -SL0 -ST2 -CUnknown // -Ubugz -PDUMBWAYTODOTHIS // ./mystic -TID9 -IP192.168.0.1 -HOSTUnknown -ML1 -SL1 -ST2 -CUnknown // -Ubugz -PIDONTUSEPASCAL // SSH: -ML1 -ST2 // Telnet: -ML0 -ST0 // Locate username (if given) in the command line // -U for (int x = 0; x < argc; x++) { /* // This doesn't work: You can give username + wrong password. // You will be identified as the wrong user at the login screen! if (strncmp("-U", argv[x], 2) == 0) { username.assign(argv[x] + 2); } */ /* Changed in latest version if (strncmp("-SL", argv[x], 3) == 0) { node = atoi(argv[x] + 3) + 1; } */ // -TID8, -TID10, -TID12 for node 1, 2, 3 if (strncmp("-TID", argv[x], 4) == 0) { node = (atoi(argv[x] + 4) - 6) / 2; } } if (node == -1) { // likely this is someone trying to run something char *args[20]; // max 20 args int x; char new_exec[] = TARGET; // build new args list args[0] = new_exec; for (x = 1; x < argc; x++) { args[x] = argv[x]; }; // null term the list args[x] = NULL; // run Mystic, run! execvp(TARGET, args); return 2; } std::string logfile; { std::ostringstream buffer; time_t now = time(NULL); struct tm *tmp; tmp = localtime(&now); // tmp->tm_mon buffer << "horrible-harry-" << tmp->tm_year + 1900 << "-" << std::setfill('0') << std::setw(2) << tmp->tm_mon + 1 << "-" << std::setfill('0') << std::setw(2) << tmp->tm_mday << "-" << node << ".log"; logfile = buffer.str(); }; if (!file_output_open((const char *)logfile.c_str())) return 2; ZF_LOGE("Horrible Harry %s", version.c_str()); for (auto cit = CONFIG.begin(); cit != CONFIG.end(); ++cit) { ZF_LOGD("Config {%s}:{%s}", (const char *)cit->first.c_str(), (const char *)cit->second.c_str()); } ZF_LOGI("Node: %d", node); if (!username.empty()) { locate_user(username.c_str()); ZF_LOGD("Username: [%s] A.K.A. [%s]", (const char *)username.c_str(), (const char *)fullname.c_str()); } open_mystic_log(); pid = forkpty(&master, NULL, NULL, NULL); // impossible to fork if (pid < 0) { return 1; } // child else if (pid == 0) { char *args[20]; // max 20 args int x; char new_exec[] = TARGET; // build new args list args[0] = new_exec; for (x = 1; x < argc; x++) { args[x] = argv[x]; }; // null term the list args[x] = NULL; // run Mystic, run! execvp(TARGET, args); } // parent else { struct termios tios, orig1; struct timeval timeout; time_t last_logscan = time(NULL); ZF_LOGD("starting"); tcgetattr(master, &tios); tios.c_lflag &= ~(ECHO | ECHONL | ICANON); /* tios.c_iflag &= ~(ICRNL | IXON | BRKINT); tios.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); tios.c_oflag &= ~(OPOST); */ tcsetattr(master, TCSAFLUSH, &tios); tcgetattr(1, &orig1); tios = orig1; tios.c_iflag &= ~(ICRNL | IXON | BRKINT); tios.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); tios.c_oflag &= ~(OPOST); // https://viewsourcecode.org/snaptoken/kilo/02.enteringRawMode.html tcsetattr(1, TCSAFLUSH, &tios); /* This doesn't need to be static -- because it is part of main. Once main ends, we're done. */ std::string buffer; buffer.reserve(BSIZE * 2); std::string play; play.reserve(4096); int zmodem = 0; // int size = 0; // use buffer.size() instead for (;;) { int time_idle; // define estruturas para o select, que serve para verificar qual // se tornou "pronto pra uso" fd_set read_fd; fd_set write_fd; fd_set except_fd; // inicializa as estruturas FD_ZERO(&read_fd); FD_ZERO(&write_fd); FD_ZERO(&except_fd); // atribui o descritor master, obtido pelo forkpty, ao read_fd FD_SET(master, &read_fd); // atribui o stdin ao read_fd FD_SET(STDIN_FILENO, &read_fd); // o descritor tem que ser unico para o programa, a documentacao // recomenda um calculo entre os descritores sendo usados + 1 /* TODO: Figure out how this would work. I'm thinking something like timeouts 30-50 seconds? And as we get closer, 15-25 seconds. if zmodem, buffer will always be empty -- we won't hold anything. */ if (buffer.size() == 0) { // buffer is empty if (zmodem) { timeout.tv_sec = 5; } else { timeout.tv_sec = randrange(10, 20); }; timeout.tv_usec = 0; time_idle = 1; } else { // buffer is not empty timeout.tv_sec = 0; timeout.tv_usec = 1; time_idle = 0; } if (last_logscan < time(NULL)) { scan_mystic_log(); if (username.empty()) last_logscan = time(NULL) + 2; else last_logscan = time(NULL) + 10; } if (select(master + 1, &read_fd, &write_fd, &except_fd, &timeout) == 0) { ZF_LOGI("TIMEOUT"); // This means timeout! if (time_idle) { if (harry_level() && !zmodem) harry_idle_event(STDOUT_FILENO); } else { ZF_LOGV("TIMEOUT buffer: %s", logrepr(buffer.c_str())); /* ZF_LOGI_MEM(buffer.data(), buffer.size(), "TIMEOUT buffer size=%lu", buffer.size()); */ play.assign(buffer); if (harry_level()) mangle(STDOUT_FILENO, play); else { write(STDOUT_FILENO, play.data(), play.size()); console_receive(&console, play); } /* ZF_LOGI("console_receive"); console_receive(&console, buffer); ZF_LOGI("write buffer"); write(STDOUT_FILENO, buffer.data(), buffer.size()); */ ZF_LOGI("buffer clear"); buffer.clear(); // size = 0; // buffer is empty now } } // read_fd esta atribuido com read_fd? if (FD_ISSET(master, &read_fd)) { // leia o que bc esta mandando // ZF_LOGD("read (%d) %d bytes", size, BSIZE - size); char read_buffer[BSIZE + 1]; int total; // We may adjust this later on (adjusting read length). if ((total = read(master, read_buffer, BSIZE)) != -1) { // Ok, we've read more into the buffer. ZF_LOGV("Read %d bytes", total); buffer.append(read_buffer, total); if (zmodem) { // Ok, we're zmodem mode -- is it time to exit? size_t zend = buffer.find("\x1b[0m"); if (zend != std::string::npos) zmodem = 0; zend = buffer.find("\x1b[1;1H"); if (zend != std::string::npos) zmodem = 0; if (!zmodem) ZF_LOGD("Zmodem end"); } else { // Should we be in zmodem mode? size_t zstart = buffer.find("**\x18" "B0"); if (zstart != std::string::npos) { zmodem = 1; ZF_LOGD("Zmodem start"); } } if (zmodem) { // ZF_LOGI("Buffer %lu bytes, zmodem...", buffer.size()); write(STDOUT_FILENO, buffer.data(), buffer.size()); // console_receive(&console, buffer); buffer.clear(); } else { // ZF_LOGV_MEM(buffer + size, total, "Read %d bytes:", total); // size += total; // ZF_LOGV_MEM(buffer, size, "Buffer now:"); size_t pos = buffer.rfind("\r\n"); // rstrnstr(buffer, size, "\r\n"); // >= 0) { if (pos != std::string::npos) { // found something! pos += 2; // play = buffer.substr() wipes out play's reserve. // play = buffer.substr(0, pos); play.assign(buffer, 0, pos); ZF_LOGI("play %lu size, %lu cap", play.size(), play.capacity()); // play.copy(buffer.data(), pos); //) = buffer.substr(0, pos); buffer.erase(0, pos); mangle(STDOUT_FILENO, play); // ZF_LOGD_MEM(buffer, pos, "mangle buffer %d bytes:", pos); // mangle(STDOUT_FILENO, buffer, pos); // memmove(buffer, buffer + pos, size - pos); // size -= pos; // } else { // ZF_LOGV("position of /r/n not found."); } // Ok, we failed to find CR+NL. What's the buffer size at? if (buffer.size() > BSIZE) { // Ok, there's something going on, and it doesn't look good // unsure if I want to feed this into the console // my guess at this point would be zmodem xfer ZF_LOGI("Buffer %lu bytes, write only...", buffer.size()); write(STDOUT_FILENO, buffer.data(), buffer.size()); console_receive(&console, buffer); buffer.clear(); } } } else break; } // read_fd esta atribuido com a entrada padrao? if (FD_ISSET(STDIN_FILENO, &read_fd)) { // leia a entrada padrao char input[BSIZE]; int r = read(STDIN_FILENO, &input, BSIZE); input[r] = 0; // e escreva no bc if (!zmodem) { if (r > 50) { ZF_LOGV("<< %d bytes", r); } else { ZF_LOGV("<< %s", repr(input)); } } write(master, &input, r); // This is INPUT from the USER // ZF_LOGI_MEM( input, strlen(input), "<< "); } } // Restore terminal tcsetattr(1, TCSAFLUSH, &orig1); ZF_LOGD("exit"); } return 0; }