diff options
Diffstat (limited to 'src/monitor-gui.cc')
| -rw-r--r-- | src/monitor-gui.cc | 487 |
1 files changed, 487 insertions, 0 deletions
diff --git a/src/monitor-gui.cc b/src/monitor-gui.cc new file mode 100644 index 0000000..682b441 --- /dev/null +++ b/src/monitor-gui.cc @@ -0,0 +1,487 @@ +// -*- mode: c++; c-basic-offset: 2; -*- + +#include "common.hh" + +#include <memory> +#include <string.h> +#include <unordered_map> +#include <vector> + +#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<unsigned long long>(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<unsigned>(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<long>(s), static_cast<unsigned long>(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<Package> packages_; + std::unordered_map<uint32_t, size_t> open_; + Observers<Listener*> listeners_; + struct timespec epoch_; + + static const std::string 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) + : monitor_(monitor) { + } + + void apply(GuiFormApply* form) override { + std::string host; + uint16_t port; + if (!parse_address(form->get_string("address"), &host, &port)) { + form->set_error("Invalid address, expects HOST[:PORT]"); + form->applied(false); + return; + } + monitor_->connect(host, port); + } + + private: + Monitor* monitor_; + }; + +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<GuiFormApply::Delegate>( + new ConnectFormDelegate(monitor_.get())); + auto lst = std::unique_ptr<GuiFormApply::Listener>( + new ConnectFormListener()); + connect_.reset( + GuiFormApply::create("Connect...", + "Enter address for monitor to connect to", + "Connect", + dlg.get())); + connect_->add_string("address", "Address", ""); + 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<AttributedText> 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<AttributedText> 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<PackageList> packages_; + std::unique_ptr<GuiMain> main_; + std::unique_ptr<GuiMenu> menu_; + std::unique_ptr<GuiStatusBar> statusbar_; + std::unique_ptr<GuiAbout> about_; + std::unique_ptr<GuiFormApply> connect_; + std::unique_ptr<Looper> looper_; + std::unique_ptr<Resolver> resolver_; + std::unique_ptr<Monitor> monitor_; + bool has_selection_; + size_t selection_; +}; + +} // namespace + +int main(int argc, char** argv) { + auto gui = std::unique_ptr<MonitorGui>(new MonitorGui()); + return gui->run(argc, argv) ? EXIT_SUCCESS : EXIT_FAILURE; +} |
