// -*- mode: c++; c-basic-offset: 2; -*- #include "common.hh" #include #include #include #include #include #include #include #include #include #include #include #include "config.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_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 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 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: 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 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* UNUSED(form), std::string const& UNUSED(id)) 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* UNUSED(form), std::string const& UNUSED(id)) 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("TransparentProxy Monitor", 800, 500)), menu_(GuiMenu::create()), statusbar_(GuiStatusBar::create()), looper_(main_->createLooper()), has_selection_(false), selection_(0) { auto file = menu_->add_menu("File"); file->add_item(ACTION_SETUP, "Setup..."); 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"); #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..."); 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_->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."); std::vector filter; filter.emplace_back(); filter.back().name = "PEM"; filter.back().masks.emplace_back("*.pem"); 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", GuiForm::FILE_OPEN, filter); connect_->enable("ssl-ca", mitm); filter.back().name = "CRT"; filter.back().masks.emplace_back("*.crt"); connect_->add_file("ssl-certs", "Certificate bundle", main_->config()->get("ssl-certs", default_cert_bundle()), "Certificate bundle to verify remote SSL connections", GuiForm::FILE_OPEN, 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()); 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()); 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(); } 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 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."); std::vector filter; filter.emplace_back(); filter.back().name = "PEM"; filter.back().masks.emplace_back("*.pem"); dlg->add_file("output", "File", "", "File to save certificate and key pair to.", GuiForm::FILE_SAVE, filter); dlg->add_listener(lst.get()); dlg->show(main_.get()); #endif // HAVE_SSL } 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); } // 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& 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)); } } void setup_proxy() { if (!proxy_config_) { proxy_config_.reset(new MemoryConfig()); } if (!proxy_logger_) { proxy_logger_.reset(new StringLogger()); } } 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_; 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; }