// -*- mode: c++; c-basic-offset: 2; -*- #include "common.hh" #include #include #include #include #include "config.hh" #include "gui_about.hh" #include "gui_formapply.hh" #include "gui_hexdump.hh" #include "gui_listmodel.hh" #include "gui_menu.hh" #include "gui_main.hh" #include "gui_statusbar.hh" #include "looper.hh" #include "monitor.hh" #include "observers.hh" #include "resolver.hh" namespace { 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"; 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; } class PackageList : public GuiListModel { public: struct Package { std::string timestamp; std::string from; std::string to; std::string size; std::string data; }; 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(Monitor::Package const& package) { auto const index = packages_.size(); open_.emplace(package.id, index); packages_.emplace_back(); auto& pkg = packages_.back(); 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); 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()) { assert(false); 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(); 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_; static const std::string empty_; }; // static const PackageList::Package PackageList::EMPTY; // static const std::string PackageList::empty_; class MonitorGui : GuiMenu::Listener, GuiMain::Listener, Monitor::Delegate { private: class ConnectFormListener : public GuiFormApply::Listener { public: 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_; }; public: MonitorGui() : packages_(new PackageList()), main_(GuiMain::create("TransparentProxy Monitor", 800, 400)), menu_(GuiMenu::create()), statusbar_(GuiStatusBar::create()), looper_(main_->createLooper()), has_selection_(false), selection_(0) { auto file = menu_->add_menu("File"); file->add_item(ACTION_CONNECT, "Connect..."); file->add_item(ACTION_DISCONNECT, "Disconnect"); file->add_separator(); file->add_item(ACTION_EXIT, "Exit"); auto edit = menu_->add_menu("Edit"); edit->add_item(ACTION_COPY_TEXT, "Copy"); edit->add_item(ACTION_COPY_RAW, "Copy binary"); edit->add_separator(); edit->add_item(ACTION_CLEAR, "Clear"); auto help = menu_->add_menu("Help"); help->add_item(ACTION_ABOUT, "About..."); 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_->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_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", "")); connect_->add_listener(lst.get()); if (connect_->show(main_.get())) { monitor_->attach(); } else { monitor_->disconnect(); } connect_.reset(); } else if (id == ACTION_DISCONNECT) { assert(monitor_); if (monitor_) { monitor_->disconnect(); } } else if (id == ACTION_COPY_TEXT) { if (!has_selection_) { assert(false); return; } 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()); main_->add_to_clipboard(str); } 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_CLEAR) { packages_->clear(); } else { assert(false); } } // GuiMain::Listener bool about_to_exit(GuiMain* main) override { assert(main_.get() == main); 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)); menu_->enable_item(ACTION_COPY_RAW, true); menu_->enable_item(ACTION_COPY_TEXT, true); } void lost_selection(GuiMain* main) override { assert(main_.get() == main); has_selection_ = false; main_->set_package(nullptr); menu_->enable_item(ACTION_COPY_RAW, false); menu_->enable_item(ACTION_COPY_TEXT, false); } // 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_CONNECT, allow_connect); menu_->enable_item(ACTION_DISCONNECT, allow_disconnect); } void error(Monitor* monitor, std::string const& UNUSED(error)) override { assert(monitor == monitor_.get()); } void package(Monitor* monitor, Monitor::Package const& package) override { assert(monitor == monitor_.get()); packages_->package(package); } 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); } } private: void setup_monitor() { if (!resolver_) { resolver_.reset(Resolver::create(looper_.get())); } if (!monitor_) { monitor_.reset(Monitor::create(looper_.get(), resolver_.get(), this)); } } 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_; bool has_selection_; size_t selection_; }; } // namespace int main(int argc, char** argv) { auto gui = std::unique_ptr(new MonitorGui()); return gui->run(argc, argv) ? EXIT_SUCCESS : EXIT_FAILURE; }