// -*- mode: c++; c-basic-offset: 2; -*- #include "common.hh" #include #include #include #include #include #include #include #include #include #include #include #if HAVE_PCAP # include # include # include #endif #include "config.hh" #include "data.hh" #include "gui_about.hh" #include "gui_config.hh" #include "gui_formapply.hh" #include "gui_hexdump.hh" #include "gui_listmodel.hh" #include "gui_menu.hh" #include "gui_message.hh" #include "gui_main.hh" #include "gui_statusbar.hh" #include "gui_textwnd.hh" #include "io.hh" #include "logger.hh" #include "looper.hh" #include "monitor.hh" #include "observers.hh" #include "proxy.hh" #include "resolver.hh" #include "ssl.hh" namespace { std::string const APP_TITLE = "TransparentProxy"; std::string const ACTION_SETUP = "setup"; std::string const ACTION_CONNECT = "connect"; std::string const ACTION_DISCONNECT = "disconnect"; std::string const ACTION_EXIT = "exit"; std::string const ACTION_ABOUT = "about"; std::string const ACTION_COPY_RAW = "copy_raw"; std::string const ACTION_COPY_TEXT = "copy_text"; std::string const ACTION_CLEAR = "clear"; std::string const ACTION_PROXY_LOG = "proxy_log"; #if HAVE_SSL std::string const ACTION_GENERATE_CA = "genca"; #endif // HAVE_SSL std::string const ACTION_NEW = "new"; std::string const ACTION_OPEN = "open"; std::string const ACTION_SAVE = "save"; std::string const ACTION_SAVE_AS = "save_as"; std::string const ACTION_JUMP = "jump"; std::string const ACTION_EXPORT_RAW = "export_raw"; std::string const ACTION_EXPORT_TEXT = "export_text"; bool valid_hostname(std::string const& host) { return !host.empty(); } bool parse_address(std::string const& addr, std::string* host, uint16_t* port) { auto i = addr.find(':'); if (i == std::string::npos) { if (!valid_hostname(addr)) return false; if (host) *host = addr; if (port) *port = 9000; return true; } auto h = addr.substr(0, i); if (!valid_hostname(h)) return false; char* end = nullptr; auto p = strtoul(addr.c_str() + i + 1, &end, 10); if (p == 0 || p > 65535 || !end || *end) return false; if (host) *host = h; if (port) *port = p; return true; } #if HAVE_SSL const char* CERT_BUNDLE[] = { "/etc/ssl/certs/ca-certificates.crt", NULL }; std::string default_cert_bundle() { static std::string cache; if (cache.empty()) { for (auto bundle = CERT_BUNDLE; *bundle; ++bundle) { if (access(*bundle, R_OK) == 0) { cache.assign(*bundle); break; } } } return cache; } #endif // HAVE_SSL class PackageList : public GuiListModel { public: static const size_t NONE; struct Package { ::Package pkg; std::string timestamp; std::string from; std::string to; std::string size; std::string data; size_t related; Package() : related(NONE) { } }; size_t rows() const override { return packages_.size(); } size_t columns() const override { return 4; } std::string header(size_t column) const override { switch (column) { case 0: return "Time"; case 1: return "From"; case 2: return "To"; case 3: return "Data"; } assert(false); return ""; } std::string data(size_t row, size_t column) const override { if (row < packages_.size()) { auto const& pkg = packages_[row]; switch (column) { case 0: return pkg.timestamp; case 1: return pkg.from; case 2: return pkg.to; case 3: return pkg.size; } } assert(false); return ""; } void add_listener(Listener* listener) override { listeners_.insert(listener); } void remove_listener(Listener* listener) override { listeners_.erase(listener); } void package(::Package const& package) { auto const index = packages_.size(); open_.emplace(package.id, index); packages_.emplace_back(); auto& pkg = packages_.back(); pkg.pkg = package; format_timestamp(&pkg.timestamp, package.timestamp); format_host_port(&pkg.from, package.source_host, package.source_port); format_host_port(&pkg.to, package.target_host, package.target_port); format_size(&pkg.size, 0, false); auto related = related_.find(pkg.to); if (related == related_.end()) { related_[pkg.from] = index; } else { pkg.related = related->second; packages_[pkg.related].related = index; related_.erase(related); } notify_added(index, index); } size_t package_data(uint32_t id, char const* data, size_t size, bool last) { auto it = open_.find(id); if (it == open_.end()) { return std::string::npos; } auto const index = it->second; auto& pkg = packages_[index]; pkg.data.append(data, size); format_size(&pkg.size, pkg.data.size(), last); if (last) open_.erase(it); notify_changed(index, index); return index; } Package const& package(size_t index) const { if (index < packages_.size()) { return packages_[index]; } assert(false); return EMPTY; } void clear() { auto last = packages_.size(); epoch_.tv_sec = 0; epoch_.tv_nsec = 0; if (last == 0) return; packages_.clear(); open_.clear(); related_.clear(); notify_removed(0, last - 1); } private: static const Package EMPTY; static void format_size(std::string* out, size_t size, bool done) { char tmp[50]; auto len = snprintf(tmp, sizeof(tmp), "%llu bytes", static_cast(size)); out->assign(tmp, len); if (!done) out->append(" ...", 4); } static void format_host_port(std::string* out, std::string const& host, uint16_t port) { out->assign(host); out->push_back(':'); char tmp[10]; auto len = snprintf(tmp, sizeof(tmp), "%u", static_cast(port)); out->append(tmp, len); } void format_timestamp(std::string* out, struct timespec const& time) { if (epoch_.tv_sec == 0 && epoch_.tv_nsec == 0) { epoch_ = time; } auto s = time.tv_sec - epoch_.tv_sec; auto n = time.tv_nsec - epoch_.tv_nsec; if (n < 0) { --s; n += 1000000000ull; } char tmp[50]; auto len = snprintf(tmp, sizeof(tmp), "%ld.%09lu", static_cast(s), static_cast(n)); out->assign(tmp, len); } void notify_added(size_t first, size_t last) { auto it = listeners_.notify(); while (it.has_next()) { it.next()->rows_added(this, first, last); } } void notify_changed(size_t first, size_t last) { auto it = listeners_.notify(); while (it.has_next()) { it.next()->rows_changed(this, first, last); } } void notify_removed(size_t first, size_t last) { auto it = listeners_.notify(); while (it.has_next()) { it.next()->rows_removed(this, first, last); } } std::vector packages_; std::unordered_map open_; Observers listeners_; struct timespec epoch_; std::unordered_map related_; static const std::string empty_; }; // static const PackageList::Package PackageList::EMPTY; // static const size_t PackageList::NONE = std::numeric_limits::max(); // static const std::string PackageList::empty_; class MemoryConfig : public GuiConfig { public: using Config::get; std::string const& get(std::string const& key, std::string const& fallback) override { auto it = memory_.find(key); if (it == memory_.end()) return fallback; return it->second; } char const* get(std::string const& key, char const* fallback) override { auto it = memory_.find(key); if (it == memory_.end()) return fallback; return it->second.c_str(); } bool is_set(std::string const& key) override { return memory_.count(key); } void set(std::string const& key, std::string const& value) override { memory_[key] = value; } void remove(std::string const& key) override { memory_.erase(key); } private: std::unordered_map memory_; }; class StringLogger : public Logger { public: void out(Level lvl, char const* format, ...) override { if (lvl == DBG) { #ifdef NDEBUG return; #endif } char* tmp; va_list args; va_start(args, format); auto ret = vasprintf(&tmp, format, args); va_end(args); if (ret == -1) return; switch (lvl) { case ERR: text_->append("Error", ATTR_ERROR); break; case WARN: text_->append("Warning", ATTR_WARNING); break; case INFO: text_->append("Info", ATTR_INFO); break; case DBG: text_->append("Debug", ATTR_DEBUG); break; } text_->append(": "); text_->append(tmp, ret); text_->append("\n"); free(tmp); } AttributedText const* text() const { return text_.get(); } void clear() { text_->reset(); } StringLogger() : ATTR_ERROR(0xff, 0, 0), ATTR_WARNING(0xff, 0xff, 0), ATTR_INFO(), ATTR_DEBUG(0, 0xff, 0), text_(AttributedText::create()) { } private: const AttributedText::Attribute ATTR_ERROR; const AttributedText::Attribute ATTR_WARNING; const AttributedText::Attribute ATTR_INFO; const AttributedText::Attribute ATTR_DEBUG; std::unique_ptr text_; }; class MonitorGui : GuiMenu::Listener, GuiMain::Listener, Monitor::Delegate, GuiTextWindow::Listener { private: class ConnectFormListener : public GuiFormApply::Listener { public: void changed(GuiForm*, std::string const&) override { } bool about_to_close(GuiForm* form) override { auto address = form->get_string("address"); if (address.empty()) { form->set_error("Empty address"); return false; } if (!parse_address(address, nullptr, nullptr)) { form->set_error("Invalid address, expects HOST[:PORT]"); return false; } return true; } }; class ConnectFormDelegate : public GuiFormApply::Delegate { public: ConnectFormDelegate(Monitor* monitor, Config* config) : monitor_(monitor), config_(config) { } void apply(GuiFormApply* form) override { std::string host; uint16_t port; auto const& addr = form->get_string("address"); if (!parse_address(addr, &host, &port)) { form->set_error("Invalid address, expects HOST[:PORT]"); form->applied(false); return; } config_->set("connect", addr); monitor_->connect(host, port); } private: Monitor* monitor_; Config* config_; }; class SetupFormListener : public GuiFormApply::Listener { public: void changed(GuiForm* form, std::string const& id) override { if (id.compare("mitm") == 0) { auto enable = form->get_bool(id); form->enable("ssl-certs", enable); form->enable("ssl-ca", enable); form->enable("unsecure", enable); } } bool about_to_close(GuiForm* form) override { auto const& port = form->get_string("port"); if (port.empty()) { form->set_error("Empty proxy port"); return false; } auto const& mitm = form->get_bool("mitm"); if (mitm) { auto const& certs = form->get_file("ssl-certs"); if (certs.empty()) { form->set_error("No SSL certificates file set"); return false; } auto const& ca = form->get_file("ssl-ca"); if (ca.empty()) { form->set_error("No SSL CA file set"); return false; } } return true; } }; class SetupFormDelegate : public GuiFormApply::Delegate { public: SetupFormDelegate(std::unique_ptr& proxy, Config* proxy_config, StringLogger* proxy_logger, Monitor* monitor, Config* config, Looper* looper, Resolver* resolver) : proxy_(proxy), proxy_config_(proxy_config), proxy_logger_(proxy_logger), monitor_(monitor), config_(config), looper_(looper), resolver_(resolver) { } void apply(GuiFormApply* form) override { auto const& bind = form->get_string("bind"); auto const& port = form->get_string("port"); if (port.empty()) { form->set_error("Empty proxy port"); form->applied(false); return; } config_->set("bind", bind); config_->set("port", port); #if HAVE_SSL auto const& mitm = form->get_bool("mitm"); config_->set("mitm", mitm); proxy_config_->set("proxy_bind", bind); proxy_config_->set("proxy_port", port); if (mitm) { auto const& certs = form->get_file("ssl-certs"); auto const& ca = form->get_file("ssl-ca"); auto const& unsecure = form->get_bool("unsecure"); if (certs.empty()) { form->set_error("No SSL certificates file set"); form->applied(false); return; } if (access(certs.c_str(), R_OK)) { form->set_error("SSL certificates file not readable"); form->applied(false); return; } if (ca.empty()) { form->set_error("No SSL CA file set"); form->applied(false); return; } if (access(ca.c_str(), R_OK)) { form->set_error("SSL CA file not readable"); form->applied(false); return; } config_->set("ssl-certs", certs); config_->set("ssl-ca", ca); config_->set("unsecure", unsecure); proxy_config_->set("ssl_cert_bundle", certs); proxy_config_->set("ssl_ca_cert", ca); proxy_config_->set("ssl_ca_key", ca); proxy_config_->set("ssl_unsecure", unsecure); } else { proxy_config_->remove("ssl_cert_bundle"); proxy_config_->remove("ssl_ca_cert"); proxy_config_->remove("ssl_ca_key"); proxy_config_->remove("ssl_unsecure"); } #endif // HAVE_SSL proxy_config_->set("__one_single_monitor", "true"); io::auto_fd proxy_fd( Proxy::setup_accept_socket(proxy_config_, proxy_logger_)); if (!proxy_fd) { auto error = proxy_logger_->text()->text(); proxy_logger_->clear(); std::string errmsg = "Unable to bind " + (bind.empty() ? "*" : bind) + ":" + port; if (!error.empty()) errmsg += "\n" + error; form->set_error(errmsg); form->applied(false); return; } int monitor_fd[2]; if (socketpair(AF_UNIX, SOCK_STREAM, 0, monitor_fd)) { form->set_error("Unable to create socketpair"); form->applied(false); return; } fcntl(monitor_fd[0], F_SETFL, O_NONBLOCK); fcntl(monitor_fd[1], F_SETFL, O_NONBLOCK); proxy_.reset(Proxy::create(proxy_config_, "", nullptr, nullptr, proxy_logger_, proxy_fd.release(), monitor_fd[0], looper_, resolver_)); monitor_->connect(monitor_fd[1]); } private: std::unique_ptr& proxy_; Config* proxy_config_; StringLogger* proxy_logger_; Monitor* monitor_; Config* config_; Looper* looper_; Resolver* resolver_; }; #if HAVE_SSL class GenerateFormListener : public GuiFormApply::Listener { public: void changed(GuiForm*, std::string const&) override { } bool about_to_close(GuiForm* form) override { auto const& output = form->get_file("output"); if (output.empty()) { form->set_error("No output file selected."); return false; } return true; } }; class GenerateFormDelegate : public GuiFormApply::Delegate { public: GenerateFormDelegate(Looper* looper, Config* config) : looper_(looper), config_(config) { } ~GenerateFormDelegate() { if (read_) looper_->remove(read_.get()); } void apply(GuiFormApply* form) override { auto const& issuer = form->get_string("issuer"); auto const& output = form->get_file("output"); if (output.empty()) { form->set_error("No ouput file selected."); form->applied(false); return; } config_->set("issuer", issuer); config_->set("genca-output", output); int fd[2]; if (pipe(fd)) { form->set_error("Unable to create pipe."); form->applied(false); return; } read_.reset(fd[0]); logger_.reset(new StringLogger()); looper_->add(read_.get(), Looper::EVENT_READ, [=](int, uint8_t) -> void { char c; if (read(read_.get(), &c, 1) != 1) { c = 0; } looper_->remove(read_.get()); read_.reset(); if (c) { form->applied(true); } else { form->set_error(logger_->text()->text()); form->applied(false); } }); // Hack to get SSL lib to initialize on the right thread SSLCert::load(Logger::null(), ""); std::thread(generate, issuer, output, logger_.get(), fd[1]).detach(); } private: static void generate(std::string const& issuer, std::string const& output, Logger* logger, int fd) { char c = doGenerate(issuer, output, logger) ? 1 : 0; write(fd, &c, 1); close(fd); } static bool doGenerate(std::string const& issuer, std::string const& output, Logger* logger) { std::unique_ptr entropy(SSLEntropy::create(logger)); if (!entropy) return false; std::string key; if (!SSLKey::generate(logger, entropy.get(), &key)) return false; std::string cert; std::unique_ptr pkey(SSLKey::load(logger, key)); if (!SSLCert::generate(logger, entropy.get(), nullptr, nullptr, issuer, pkey.get(), &cert)) return false; std::ofstream of(output); of << cert << '\n' << key << std::endl; return of.good(); } io::auto_fd read_; Looper* looper_; Config* config_; std::unique_ptr logger_; }; #endif // HAVE_SSL public: MonitorGui() : packages_(new PackageList()), main_(GuiMain::create(APP_TITLE, 800, 500)), menu_(GuiMenu::create()), statusbar_(GuiStatusBar::create()), looper_(main_->create_looper()), has_selection_(false), has_related_(false), selection_(0), modified_(false) { pem_filter_.emplace_back(); pem_filter_.back().name = "PEM (*.pem)"; pem_filter_.back().masks.emplace_back("*.pem"); pem_filter_.emplace_back(); pem_filter_.back().name = "All files"; pem_filter_.back().masks.emplace_back("*.*"); crt_filter_.emplace_back(); crt_filter_.back().name = "Certificates (*.crt)"; crt_filter_.back().masks.emplace_back("*.crt"); crt_filter_.emplace_back(); crt_filter_.back().name = "All files"; crt_filter_.back().masks.emplace_back("*.*"); txt_filter_.emplace_back(); txt_filter_.back().name = "Text files (*.txt)"; txt_filter_.back().masks.emplace_back("*.txt"); txt_filter_.emplace_back(); txt_filter_.back().name = "All files"; txt_filter_.back().masks.emplace_back("*.*"); raw_filter_.emplace_back(); raw_filter_.back().name = "All files"; raw_filter_.back().masks.emplace_back("*.*"); auto file = menu_->add_menu("File"); file->add_item(ACTION_NEW, "New", GuiMenu::Shortcut(GuiMenu::CTRL, 'N')); file->add_item(ACTION_OPEN, "Open...", GuiMenu::Shortcut(GuiMenu::CTRL, 'O')); file->add_item(ACTION_SAVE, "Save", GuiMenu::Shortcut(GuiMenu::CTRL, 'S')); file->add_item(ACTION_SAVE_AS, "Save As...", GuiMenu::Shortcut(GuiMenu::CTRL | GuiMenu::SHIFT, 'S')); file->add_separator(); file->add_item(ACTION_EXIT, "Quit", GuiMenu::Shortcut(GuiMenu::CTRL, 'Q')); menu_->enable_item(ACTION_SAVE, false); file_filter_.emplace_back(); file_filter_.back().name = "TransparentProxy Packages (*.tpp)"; file_filter_.back().masks.emplace_back("*.tpp"); #if HAVE_PCAP file_filter_.emplace_back(); file_filter_.back().name = "Packet Capture (*.pcap)"; file_filter_.back().masks.emplace_back("*.pcap"); #endif // HAVE_PCAP file_filter_.emplace_back(); file_filter_.back().name = "All files"; file_filter_.back().masks.emplace_back("*.*"); auto proxy = menu_->add_menu("Proxy"); proxy->add_item(ACTION_SETUP, "Setup..."); proxy->add_item(ACTION_CONNECT, "Connect...", GuiMenu::Shortcut(GuiMenu::CTRL, 'C')); proxy->add_item(ACTION_DISCONNECT, "Disconnect", GuiMenu::Shortcut(GuiMenu::CTRL, 'D')); auto edit = menu_->add_menu("Edit"); edit->add_item(ACTION_COPY_TEXT, "Copy", GuiMenu::Shortcut(GuiMenu::CTRL, 'C')); edit->add_item(ACTION_COPY_RAW, "Copy binary", GuiMenu::Shortcut(GuiMenu::CTRL | GuiMenu::SHIFT, 'C')); edit->add_separator(); edit->add_item(ACTION_EXPORT_TEXT, "Export...", GuiMenu::Shortcut(GuiMenu::CTRL, 'E')); edit->add_item(ACTION_EXPORT_RAW, "Export binary...", GuiMenu::Shortcut(GuiMenu::CTRL | GuiMenu::SHIFT, 'E')); edit->add_separator(); edit->add_item(ACTION_JUMP, "Jump to related", GuiMenu::Shortcut(GuiMenu::CTRL, ' ')); edit->add_separator(); edit->add_item(ACTION_CLEAR, "Clear"); #if HAVE_SSL auto tools = menu_->add_menu("Tools"); tools->add_item(ACTION_GENERATE_CA, "Generate CA..."); #endif // HAVE_SSL auto help = menu_->add_menu("Help"); help->add_item(ACTION_ABOUT, "About...", GuiMenu::Shortcut(0, 1)); help->add_item(ACTION_PROXY_LOG, "Proxy log..."); main_->set_menu(menu_.get()); main_->set_statusbar(statusbar_.get()); main_->set_split(0.7); main_->set_listmodel(packages_.get()); statusbar_->set_status("Not connected"); menu_->enable_item(ACTION_DISCONNECT, false); menu_->enable_item(ACTION_COPY_RAW, false); menu_->enable_item(ACTION_COPY_TEXT, false); menu_->enable_item(ACTION_EXPORT_RAW, false); menu_->enable_item(ACTION_EXPORT_TEXT, false); menu_->enable_item(ACTION_JUMP, false); menu_->add_listener(this); main_->add_listener(this); } bool run(int argc, char** argv) { return main_->run(argc, argv); } // GuiMenu::Listener void item_activated(const std::string& id) override { if (id == ACTION_EXIT) { main_->exit(); } else if (id == ACTION_ABOUT) { if (!about_) { about_.reset(GuiAbout::create("TransparentProxy Monitor", VERSION, "Joel Klinghed", "the_jk@yahoo.com", nullptr)); } main_->show(about_.get()); } else if (id == ACTION_SETUP) { setup_monitor(); setup_proxy(); proxy_logger_->clear(); auto dlg = std::unique_ptr( new SetupFormDelegate(proxy_, proxy_config_.get(), proxy_logger_.get(), monitor_.get(), main_->config(), looper_.get(), resolver_.get())); auto lst = std::unique_ptr( new SetupFormListener()); connect_.reset( GuiFormApply::create("Setup...", "Setup a proxy to start monitoring traffic.", "Setup", dlg.get())); connect_->add_string("bind", "Address", main_->config()->get("bind", ""), "Address to listen for proxy connections on." " Leave empty to listen on all interfaces."); connect_->add_string("port", "Port", main_->config()->get("port", "8080"), "Port to listen for proxy connections on."); #if HAVE_SSL bool mitm = main_->config()->get("mitm", false); connect_->add_bool("mitm", "Intercept SSL traffic", mitm, "If enabled SSL connections will be intercepted" " by the proxy to log unencrypted traffic."); connect_->add_file("ssl-ca", "Certificate Authority", main_->config()->get("ssl-ca", main_->config()->get("genca-output", "")), "CA and key to sign all fake server certificates with", GuiFile::FILE_OPEN, pem_filter_); connect_->enable("ssl-ca", mitm); connect_->add_file("ssl-certs", "Certificate bundle", main_->config()->get("ssl-certs", default_cert_bundle()), "Certificate bundle to verify remote SSL connections", GuiFile::FILE_OPEN, crt_filter_); connect_->enable("ssl-certs", mitm); connect_->add_bool("unsecure", "Allow unsecure remote connections", main_->config()->get("unsecure", false), "Allow deprecated protocols such as SSLv3 and " " self-signed or missmatched certificates"); connect_->enable("unsecure", mitm); #endif // HAVE_SSL connect_->add_listener(lst.get()); statusbar_->clear_override(); if (connect_->show(main_.get())) { monitor_->attach(); connect_.reset(); } else { connect_.reset(); monitor_->disconnect(); proxy_.reset(); } } else if (id == ACTION_CONNECT) { setup_monitor(); auto dlg = std::unique_ptr( new ConnectFormDelegate(monitor_.get(), main_->config())); auto lst = std::unique_ptr( new ConnectFormListener()); connect_.reset( GuiFormApply::create("Connect...", "Enter address for monitor to connect to", "Connect", dlg.get())); connect_->add_string("address", "Address", main_->config()->get("connect", ""), "Host and optional port (defaults to 9000)"); connect_->add_listener(lst.get()); statusbar_->clear_override(); if (connect_->show(main_.get())) { monitor_->attach(); } else { monitor_->disconnect(); } connect_.reset(); } else if (id == ACTION_DISCONNECT) { assert(monitor_); if (monitor_) { monitor_->disconnect(); } proxy_.reset(); statusbar_->clear_override(); } else if (id == ACTION_COPY_TEXT) { if (!has_selection_) { assert(false); return; } main_->add_to_clipboard(text_for_selection()); } else if (id == ACTION_COPY_RAW) { if (!has_selection_) { assert(false); return; } auto& pkg = packages_->package(selection_); main_->add_to_clipboard(pkg.data, "application/octet-stream"); } else if (id == ACTION_EXPORT_TEXT) { if (!has_selection_) { assert(false); return; } export_text(text_for_selection(), txt_filter_); } else if (id == ACTION_EXPORT_RAW) { if (!has_selection_) { assert(false); return; } auto& pkg = packages_->package(selection_); export_text(pkg.data, raw_filter_); } else if (id == ACTION_JUMP) { if (!has_selection_) { assert(false); return; } auto& pkg = packages_->package(selection_); if (pkg.related == PackageList::NONE) { assert(false); return; } main_->select_row(pkg.related); } else if (id == ACTION_CLEAR) { packages_->clear(); set_modified(true); } else if (id == ACTION_PROXY_LOG) { if (!proxy_logwnd_) { if (!proxy_logger_) proxy_logger_.reset(new StringLogger()); proxy_logwnd_.reset(GuiTextWindow::create( "Proxy log", 500, 200, proxy_logger_->text())); proxy_logwnd_->add_listener(this); proxy_logwnd_->show(main_.get()); } else { proxy_logwnd_->focus(); } #if HAVE_SSL } else if (id == ACTION_GENERATE_CA) { auto del = std::unique_ptr( new GenerateFormDelegate(looper_.get(), main_->config())); auto lst = std::unique_ptr( new GenerateFormListener()); auto dlg = std::unique_ptr( GuiFormApply::create("Generate CA...", "Generate a certificate and key pair " "and save to file.", "Generate", del.get())); dlg->add_string("issuer", "Issuer name", main_->config()->get("issuer", ""), "Issuer name to user instead of default."); dlg->add_file("output", "File", "", "File to save certificate and key pair to.", GuiFile::FILE_SAVE, pem_filter_); dlg->add_listener(lst.get()); dlg->show(main_.get()); #endif // HAVE_SSL } else if (id == ACTION_NEW) { new_file(); } else if (id == ACTION_OPEN) { load_file(); } else if (id == ACTION_SAVE) { save_file(); } else if (id == ACTION_SAVE_AS) { save_as_file(); } else { assert(false); } } // GuiMain::Listener bool about_to_exit(GuiMain* main) override { assert(main_.get() == main); if (packages_->rows() > 0) { if (abort_if_modified()) return false; } return true; } void selected_row(GuiMain* main, size_t index) override { assert(main_.get() == main); if (has_selection_ && selection_ == index) return; has_selection_ = true; selection_ = index; auto& pkg = packages_->package(index); std::unique_ptr text(AttributedText::create()); HexDump::write(text.get(), HexDump::ADDRESS | HexDump::CHARS, pkg.data); main_->set_package(std::move(text)); has_related_ = pkg.related != PackageList::NONE; menu_->enable_item(ACTION_COPY_RAW, true); menu_->enable_item(ACTION_COPY_TEXT, true); menu_->enable_item(ACTION_EXPORT_RAW, true); menu_->enable_item(ACTION_EXPORT_TEXT, true); menu_->enable_item(ACTION_JUMP, has_related_); } void lost_selection(GuiMain* main) override { assert(main_.get() == main); has_selection_ = false; has_related_ = false; main_->set_package(nullptr); menu_->enable_item(ACTION_COPY_RAW, false); menu_->enable_item(ACTION_COPY_TEXT, false); menu_->enable_item(ACTION_EXPORT_RAW, false); menu_->enable_item(ACTION_EXPORT_TEXT, false); menu_->enable_item(ACTION_JUMP, false); } void open(GuiMain*, std::string const& file) override { if (abort_if_modified()) return; file_ = file; if (!load(file_, packages_.get())) { file_.clear(); } modified_ = false; update_title(); } // GuiTextWindow::Listener bool about_to_close(GuiTextWindow* wnd) override { assert(proxy_logwnd_.get() == wnd); proxy_logwnd_.reset(); return true; } // Monitor::Delegate void state(Monitor* monitor, Monitor::State state) override { assert(monitor == monitor_.get()); if (connect_) { switch (state) { case Monitor::DISCONNECTED: connect_->set_error("Unable to connect, is the proxy running" " and listening for monitors?"); connect_->applied(false); break; case Monitor::CONNECTED: connect_->applied(true); break; case Monitor::CONNECTING: case Monitor::ATTACHED: break; } } bool allow_connect, allow_disconnect; switch (state) { case Monitor::DISCONNECTED: statusbar_->set_status("Not connected"); allow_connect = true; allow_disconnect = false; break; case Monitor::CONNECTED: statusbar_->set_status("Connected"); allow_connect = false; allow_disconnect = true; break; case Monitor::CONNECTING: statusbar_->set_status("Connecting..."); allow_connect = false; allow_disconnect = false; break; case Monitor::ATTACHED: statusbar_->set_status("Connected and attached"); allow_connect = false; allow_disconnect = true; break; } menu_->enable_item(ACTION_SETUP, allow_connect); menu_->enable_item(ACTION_CONNECT, allow_connect); menu_->enable_item(ACTION_DISCONNECT, allow_disconnect); } void error(Monitor* monitor, std::string const& error) override { assert(monitor == monitor_.get()); if (!proxy_logger_) proxy_logger_.reset(new StringLogger()); proxy_logger_->out(Logger::ERR, "%s", error.c_str()); statusbar_->set_override(error); } void package(Monitor* monitor, ::Package const& package) override { assert(monitor == monitor_.get()); packages_->package(package); set_modified(true); if (has_selection_ && !has_related_) { auto const& pkg = packages_->package(selection_); menu_->enable_item(ACTION_JUMP, pkg.related != PackageList::NONE); } } void package_data(Monitor* monitor, uint32_t id, char const* data, size_t size, bool last) override { assert(monitor == monitor_.get()); auto index = packages_->package_data(id, data, size, last); if (has_selection_ && index == selection_) { selected_row(nullptr, index); } set_modified(true); } private: void setup_monitor() { if (!resolver_) { resolver_.reset(Resolver::create(looper_.get())); } if (!monitor_) { monitor_.reset(Monitor::create(looper_.get(), resolver_.get(), this)); } } void setup_proxy() { if (!proxy_config_) { proxy_config_.reset(new MemoryConfig()); } if (!proxy_logger_) { proxy_logger_.reset(new StringLogger()); } } void set_modified(bool modified) { if (modified_ == modified) return; modified_ = modified; update_title(); } bool abort_if_modified() { if (!modified_) return false; std::unique_ptr form( GuiForm::create("Confirm?", "If you continue you will lose unsaved packages")); return !form->show(main_.get()); } void update_title() { if (file_.empty()) { main_->set_title(APP_TITLE); menu_->enable_item(ACTION_SAVE, false); } else { auto pos = file_.find_last_of('/'); pos = pos == std::string::npos ? 0 : pos + 1; auto title = file_.substr(pos); if (modified_) title.push_back('*'); title.append(" - "); title.append(APP_TITLE); main_->set_title(title); menu_->enable_item(ACTION_SAVE, modified_); } } void new_file() { if (abort_if_modified()) return; file_.clear(); modified_ = false; packages_->clear(); update_title(); } void load_file() { if (abort_if_modified()) return; file_ = main_->file_dialog("Open File", "", GuiFile::FILE_OPEN, file_filter_); if (file_.empty()) return; packages_->clear(); if (!load(file_, packages_.get())) { file_.clear(); } modified_ = false; update_title(); } void save_file() { if (file_.empty()) { assert(false); return; } if (save(packages_.get(), file_)) { modified_ = false; update_title(); } } void save_as_file() { auto file = main_->file_dialog("Save File", file_, GuiFile::FILE_SAVE, file_filter_); if (file.empty()) return; file_ = file; if (save(packages_.get(), file)) { file_ = file; modified_ = false; update_title(); } } void show_error(std::string const& error) { std::unique_ptr message(GuiMessage::create(GuiMessage::ERROR, "Error", error)); message->show(main_.get()); } std::string text_for_selection() { auto& pkg = packages_->package(selection_); std::unique_ptr text(AttributedText::create()); HexDump::write(text.get(), HexDump::ADDRESS | HexDump::CHARS, pkg.data); std::string str; str.append("From: ").append(pkg.from).push_back('\n'); str.append("To : ").append(pkg.to).push_back('\n'); str.append("At : ").append(pkg.timestamp).push_back('\n'); str.append(text->text()); return str; } void export_text(std::string const& data, std::vector const& filter) { auto file = main_->file_dialog("Export", "", GuiFile::FILE_SAVE, filter); if (file.empty()) return; std::ofstream f(file); if (!f.good()) { show_error("Unable to open " + file + " for writing."); return; } f.write(data.data(), data.size()); if (!f.good()) { show_error("Error writing to " + file); return; } } #if HAVE_PCAP static bool is_pcap(std::string const& file) { return file.size() >= 5 && file.compare(file.size() - 5, 5, ".pcap") == 0; } bool load_pcap(std::string const& file, PackageList* packages) { char errbuf[PCAP_ERRBUF_SIZE]; auto pcap = pcap_open_offline(file.c_str(), errbuf); if (!pcap) { show_error(errbuf); return false; } size_t offset; switch (pcap_datalink(pcap)) { case DLT_RAW: offset = 0; break; case DLT_EN10MB: offset = 14; break; default: snprintf(errbuf, sizeof(errbuf), "Unsupported dataline: %d", pcap_datalink(pcap)); show_error(errbuf); return false; } while (true) { struct pcap_pkthdr* header; u_char const* data; auto ret = pcap_next_ex(pcap, &header, &data); if (ret == 1) { auto len = std::min(header->caplen, header->len); if (len < offset) continue; ::Package pkg; pkg.timestamp.tv_sec = header->ts.tv_sec; pkg.timestamp.tv_nsec = header->ts.tv_usec * 1000; auto len_ip = is_ipv4(data + offset, len - offset, &pkg); if (len_ip < 0) continue; auto len_tcp = is_tcp(data + offset + len_ip, len - len_ip - offset, &pkg); if (len_tcp < 0) continue; if (offset + len_ip + len_tcp == len) continue; packages->package(pkg); packages->package_data(pkg.id, reinterpret_cast(data) + offset + len_ip + len_tcp, len - offset - len_ip - len_tcp, true); } else if (ret == -2) { break; } else if (ret == -1) { show_error(pcap_geterr(pcap)); pcap_close(pcap); return false; } else { assert(false); show_error("Unexpected return value"); pcap_close(pcap); return false; } } pcap_close(pcap); return true; } static ssize_t is_ipv4(const uint8_t* data, size_t max, ::Package* package) { if (max < 1) return -1; if ((data[0] >> 4) != 4) return -1; uint8_t ihl = data[0] & 0xf; if (ihl < 5) return -1; if (ihl * 4 > max) return -1; if (data[9] != 0x6) return -1; char tmp[INET_ADDRSTRLEN]; struct in_addr addr; addr.s_addr = htonl(read_u32(data + 12)); if (!inet_ntop(AF_INET, &addr, tmp, sizeof(tmp))) return -1; package->source_host.assign(tmp); addr.s_addr = htonl(read_u32(data + 16)); if (!inet_ntop(AF_INET, &addr, tmp, sizeof(tmp))) return -1; package->target_host.assign(tmp); return ihl * 4; } static ssize_t is_tcp(const uint8_t* data, size_t max, ::Package* package) { if (max < 20) return -1; uint8_t data_offset = data[12] >> 4; if (data_offset < 5) return -1; if (data_offset * 4 > max) return -1; package->source_port = read_u16(data); package->target_port = read_u16(data + 2); package->id = read_u32(data + 4); return data_offset * 4; } bool save_pcap(PackageList const* packages, std::string const& file) { auto pcap = pcap_open_dead(DLT_RAW, 65535); auto dumper = pcap_dump_open(pcap, file.c_str()); if (!dumper) { show_error(pcap_geterr(pcap)); pcap_close(pcap); return false; } uint16_t id = 0; std::string data; std::unordered_map cache; for (size_t index = 0; index < packages->rows(); ++index) { auto const& pkg = packages->package(index); struct pcap_pkthdr header; header.ts.tv_sec = pkg.pkg.timestamp.tv_sec; header.ts.tv_usec = pkg.pkg.timestamp.tv_nsec / 1000; uint8_t ip[20]; ip[0] = (4 << 4) | 5; // Version | IHL ip[1] = 0; // DCSP | ECN ip[6] = 0; // Flags | Fragment Offset ip[7] = 0; // Fragment Offset ip[8] = 127; // TTL ip[9] = 6; // Protocol (TCP) auto it = cache.find(pkg.pkg.source_host); if (it == cache.end()) { it = cache.emplace(pkg.pkg.source_host, get_ip(pkg.pkg.source_host)).first; } write_u32(ip + 12, it->second); it = cache.find(pkg.pkg.target_host); if (it == cache.end()) { it = cache.emplace(pkg.pkg.target_host, get_ip(pkg.pkg.target_host)).first; } write_u32(ip + 16, it->second); uint8_t tcp[20]; write_u16(tcp, pkg.pkg.source_port); write_u16(tcp + 2, pkg.pkg.target_port); write_u32(tcp + 8, 0); // ACK tcp[12] = (5 << 4); // Data offset | NS write_u16(tcp + 14, 0); // Window size write_u16(tcp + 18, 0); // Urgent pointer size_t offset = 0; size_t len = pkg.data.size(); while (offset < len) { size_t partlen = std::min(len - offset, 65535 - sizeof(ip) - sizeof(tcp)); header.len = sizeof(ip) + sizeof(tcp) + partlen; header.caplen = header.len; write_u16(ip + 2, header.len); // Total Length write_u16(ip + 4, id); // Identificiation ++id; write_u16(ip + 10, 0); uint16_t checksum = ipv4_checksum(ip, sizeof(ip)); write_u16(ip + 10, checksum); // Checksum write_u32(tcp + 4, pkg.pkg.id); tcp[13] = ((offset == 0) ? 2 : 0) | ((offset + partlen == len) ? 1 : 0); write_u16(tcp + 16, 0); checksum = tcp_checksum(ip, tcp, reinterpret_cast(pkg.data.data()) + offset, partlen); write_u16(tcp + 16, checksum); // Checksum data.assign(reinterpret_cast(ip), sizeof(ip)); data.append(reinterpret_cast(tcp), sizeof(tcp)); data.append(pkg.data.data() + offset, partlen); pcap_dump(reinterpret_cast(dumper), &header, reinterpret_cast(data.data())); offset += partlen; } } pcap_dump_close(dumper); pcap_close(pcap); return true; } static uint16_t ipv4_checksum(uint8_t const* header, size_t size) { assert((size % 2) == 0); uint16_t sum = 0; for (size_t i = 0; i < size; i += 2) { sum += (header[i] << 8) | header[i + 1]; } return ~sum; } static uint32_t get_ip(std::string const& host) { struct addrinfo hints; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; struct addrinfo* ret; if (getaddrinfo(host.c_str(), nullptr, &hints, &ret)) { return 0; } uint32_t ip = 0; for (auto p = ret; p; p = p->ai_next) { if (p->ai_family == AF_INET) { ip = ntohl( reinterpret_cast(p->ai_addr)->sin_addr.s_addr); break; } } freeaddrinfo(ret); return ip; } static uint16_t tcp_checksum(uint8_t const* ip, uint8_t const* tcp, uint8_t const* data, size_t size) { uint16_t sum = 0; sum += ip[12] << 8 | ip[13]; sum += ip[14] << 8 | ip[15]; sum += ip[16] << 8 | ip[17]; sum += ip[18] << 8 | ip[19]; sum += 0x06; sum += 20; for (size_t i = 0; i < 20; i += 2) { sum += (tcp[i] << 8) | tcp[i + 1]; } for (size_t i = 0; i < size / 2; ++i) { sum += (data[i * 2] << 8) | data[i * 2 + 1]; } if (size % 2) sum += data[size - 1] << 8; return ~sum; } #else // HAVE_PCAP static bool is_pcap(std::string const&) { return false; } static bool load_pcap(std::string const&, PackageList*) { return false; } static bool save_pcap(PackageList const*, std::string const&) { return false; } #endif // HAVE_PCAP bool load(std::string const& file, PackageList* packages) { if (is_pcap(file)) return load_pcap(file, packages); std::ifstream in(file); if (!in.good()) { show_error("Unable to open " + file + " for reading"); return false; } uint8_t header[8]; in.read(reinterpret_cast(header), 8); if (!in.good() || memcmp(header, "TPP", 3) || header[3] != 1) { show_error("Not a valid package file: " +file); return false; } uint32_t count = read_u32(header + 4); bool good = true; while (count--) { good = false; in.read(reinterpret_cast(header), 8); if (!in.good()) break; size_t size = read_u64(header); if (size == 0) break; uint8_t buf[8192]; size_t avail = std::min(size, sizeof(buf)); in.read(reinterpret_cast(buf), avail); if (!in.good()) break; ::Package pkg; auto pkg_size = read_package(&pkg, buf, avail); if (pkg_size == 0) { if (avail == size) break; size_t need = std::min(static_cast(1024) * 1024, size); std::unique_ptr mem(new uint8_t[need]); memcpy(mem.get(), buf, avail); in.read(reinterpret_cast(mem.get()) + avail, need - avail); if (!in.good()) break; pkg_size = read_package(&pkg, mem.get(), need); if (pkg_size == 0) break; packages->package(pkg); packages->package_data(pkg.id, reinterpret_cast(mem.get()) + pkg_size, need - pkg_size, need == size); size -= need; } else { packages->package(pkg); packages->package_data(pkg.id, reinterpret_cast(buf) + pkg_size, avail - pkg_size, avail == size); size -= avail; } while (size) { avail = std::min(sizeof(buf), size); in.read(reinterpret_cast(buf), avail); if (!in.good()) break; packages->package_data(pkg.id, reinterpret_cast(buf), avail, avail == size); size -= avail; } if (size) break; good = true; } if (good) { in.read(reinterpret_cast(header), 8); if (!in.good() || read_u64(header) != 0) good = false; } if (!good) { show_error("Error reading from " + file); return false; } return true; } bool save(PackageList const* packages, std::string const& file) { if (is_pcap(file)) return save_pcap(packages, file); std::ofstream out(file); if (!out.good()) { show_error("Unable to open " + file + " for writing"); return false; } uint8_t header[8]; memcpy(header, "TPP", 3); header[3] = 0x1; // Version write_u32(header + 4, packages->rows()); // Count out.write(reinterpret_cast(header), 8); for (size_t i = 0; i < packages->rows(); ++i) { auto const& pkg = packages->package(i); uint8_t buf[8192]; std::unique_ptr backup; uint8_t* ptr = buf; size_t need = write_package(pkg.pkg, buf, sizeof(buf)); if (need > sizeof(buf)) { backup.reset(new uint8_t[need]); ptr = backup.get(); write_package(pkg.pkg, ptr, need); } write_u64(header, need + pkg.data.size()); out.write(reinterpret_cast(header), 8); out.write(reinterpret_cast(ptr), need); backup.reset(); out.write(pkg.data.data(), pkg.data.size()); } write_u64(header, 0); // EOF out.write(reinterpret_cast(header), 8); if (!out.good()) { show_error("Error writing " + file); return false; } return true; } std::unique_ptr packages_; std::unique_ptr main_; std::unique_ptr menu_; std::unique_ptr statusbar_; std::unique_ptr about_; std::unique_ptr connect_; std::unique_ptr looper_; std::unique_ptr resolver_; std::unique_ptr monitor_; std::unique_ptr proxy_config_; std::unique_ptr proxy_logger_; std::unique_ptr proxy_; std::unique_ptr proxy_logwnd_; std::vector pem_filter_; std::vector crt_filter_; bool has_selection_; bool has_related_; size_t selection_; std::vector file_filter_; std::string file_; bool modified_; std::vector txt_filter_; std::vector raw_filter_; }; } // namespace int main(int argc, char** argv) { auto gui = std::unique_ptr(new MonitorGui()); return gui->run(argc, argv) ? EXIT_SUCCESS : EXIT_FAILURE; }