/* This file is part of status-area-applet-tor. * * Copyright (C) 2010-2011 Philipp Zabel * * status-area-applet-tor is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * status-area-applet-tor is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with status-area-applet-tor. If not, see . */ [Compact] class ProxyBackup { public bool use_http_proxy; public string http_host; public string socks_host; public string secure_host; public int http_port; public int socks_port; public int secure_port; public string mode; } class TorStatusMenuItem : HD.StatusMenuItem { private const string STATUSMENU_TOR_LIBOSSO_SERVICE_NAME = "tor_status_menu_item"; private const int STATUS_MENU_ICON_SIZE = 48; private const int STATUS_AREA_ICON_SIZE = 18; private const string GCONF_DIR_TOR = "/apps/maemo/tor"; private const string GCONF_KEY_TOR_ENABLED = GCONF_DIR_TOR + "/enabled"; private const string GCONF_KEY_BRIDGES = GCONF_DIR_TOR + "/bridges"; private const string GCONF_KEY_EXITNODES = GCONF_DIR_TOR + "/exit_nodes"; private const string GCONF_DIR_PROXY_HTTP = "/system/http_proxy"; private const string GCONF_KEY_PROXY_HTTP_ENABLED = GCONF_DIR_PROXY_HTTP + "/use_http_proxy"; private const string GCONF_KEY_PROXY_HTTP_HOST = GCONF_DIR_PROXY_HTTP + "/host"; private const string GCONF_KEY_PROXY_HTTP_PORT = GCONF_DIR_PROXY_HTTP + "/port"; private const string GCONF_DIR_PROXY = "/system/proxy"; private const string GCONF_KEY_PROXY_MODE = GCONF_DIR_PROXY + "/mode"; private const string GCONF_KEY_PROXY_SOCKS_HOST = GCONF_DIR_PROXY + "/socks_host"; private const string GCONF_KEY_PROXY_SOCKS_PORT = GCONF_DIR_PROXY + "/socks_port"; private const string GCONF_KEY_PROXY_SECURE_HOST = GCONF_DIR_PROXY + "/secure_host"; private const string GCONF_KEY_PROXY_SECURE_PORT = GCONF_DIR_PROXY + "/secure_port"; // Widgets Hildon.Button button; Gtk.Label log_label; // Icons Gdk.Pixbuf icon_connecting; Gdk.Pixbuf icon_connected; Gtk.Image icon_enabled; Gtk.Image icon_disabled; // ConIc, GConf and Osso context Osso.Context osso; GConf.Client gconf; ConIc.Connection conic; bool conic_connected; // Internal state bool tor_enabled; bool tor_connected; Pid tor_pid; int tor_stdout; Pid polipo_pid; ProxyBackup backup; string tor_log; TorControl.Connection tor_control; string password; /** * Update status area icon and status menu button value */ private bool update_status () { try { if (tor_enabled && tor_connected && icon_connected == null) { var icon_theme = Gtk.IconTheme.get_default (); var pixbuf = icon_theme.load_icon ("statusarea_tor_connected", STATUS_AREA_ICON_SIZE, Gtk.IconLookupFlags.NO_SVG); icon_connected = pixbuf; } if (tor_enabled && !tor_connected && icon_connecting == null) { var icon_theme = Gtk.IconTheme.get_default (); var pixbuf = icon_theme.load_icon ("statusarea_tor_connecting", STATUS_AREA_ICON_SIZE, Gtk.IconLookupFlags.NO_SVG); icon_connecting = pixbuf; } if (tor_enabled && icon_enabled == null) { var icon_theme = Gtk.IconTheme.get_default(); var pixbuf = icon_theme.load_icon ("statusarea_tor_enabled", STATUS_MENU_ICON_SIZE, Gtk.IconLookupFlags.NO_SVG); icon_enabled = new Gtk.Image.from_pixbuf (pixbuf); } if (!tor_enabled && icon_disabled == null) { var icon_theme = Gtk.IconTheme.get_default(); var pixbuf = icon_theme.load_icon ("statusarea_tor_disabled", STATUS_MENU_ICON_SIZE, Gtk.IconLookupFlags.NO_SVG); icon_disabled = new Gtk.Image.from_pixbuf (pixbuf); } } catch (Error e) { critical (e.message); var icon_theme = Gtk.IconTheme.get_default (); icon_theme.rescan_if_needed (); Timeout.add_seconds (1, update_status); return false; } if (conic_connected && tor_enabled) { set_status_area_icon (tor_connected ? icon_connected : icon_connecting); button.set_value (tor_connected ? _("Connected") : _("Connecting ...")); } else { set_status_area_icon (null); button.set_value (tor_enabled ? _("Disconnected") : _("Disabled")); } button.set_image (tor_enabled ? icon_enabled : icon_disabled); return false; } /** * Callback for Tor daemon line output */ private bool tor_io_func (IOChannel source, IOCondition condition) { if ((condition & (IOCondition.IN | IOCondition.PRI)) != 0) { string line = null; size_t length; try { /* var status = */ source.read_line (out line, out length, null); tor_log += line; if (log_label != null) log_label.label = tor_log; if ("[notice]" in line) { if ("Bootstrapped 100%" in line) { tor_connected = true; proxy_setup (); update_status (); } if ("Opening Control listener on 127.0.0.1:9051" in line) { tor_control = new TorControl.Connection (); tor_control_auth.begin (); } } else { // FIXME Hildon.Banner.show_information (null, null, "DEBUG: %s".printf (line)); } } catch (Error e) { // FIXME Hildon.Banner.show_information (null, null, "Error: %s".printf (e.message)); } } if ((condition & (IOCondition.ERR | IOCondition.HUP | IOCondition.NVAL)) != 0) { return false; } return true; } /** * Authenticate with Tor on the control channel */ private async void tor_control_auth () throws Error { yield tor_control.authenticate_async (password); var bridges = gconf.get_list (GCONF_KEY_BRIDGES, GConf.ValueType.STRING); if (bridges.length () > 0) { // Enable bridge relays tor_control.set_conf_list ("Bridge", bridges); tor_control.set_conf_bool ("UseBridges", true); bool use = yield tor_control.get_conf_bool_async ("UseBridges"); if (!use) { Hildon.Banner.show_information (null, null, "Failed to set up bridge relays"); } } var exits = gconf.get_list (GCONF_KEY_EXITNODES, GConf.ValueType.STRING); if (exits.length () > 0) { // Enable strict exit nodes tor_control.set_conf_list ("ExitNodes", exits); tor_control.set_conf_bool ("StrictExitNodes", true); bool strict = yield tor_control.get_conf_bool_async ("StrictExitNodes"); if (!strict) { Hildon.Banner.show_information (null, null, "Failed to set up strict exit nodes"); } } } /** * Start Tor and setup proxy settings */ private void start_tor () { try { if (tor_pid == (Pid) 0) { string[] tor_hash_argv = { "/usr/sbin/tor", "--hash-password", "", null }; var tv = TimeVal (); Random.set_seed ((uint32) tv.tv_usec); password = "tor-status-%8x".printf (Random.next_int ()); tor_hash_argv[2] = password; string hash; Process.spawn_sync ("/tmp", tor_hash_argv, null, 0, null, out hash); hash = hash.str ("\n16:").offset (1).replace ("\n", ""); if (hash == null) { Hildon.Banner.show_information (null, null, "Failed to get hash"); return; } string[] tor_argv = { "/usr/sbin/tor", "--ControlPort", "9051", "--HashedControlPassword", "", null }; tor_argv[4] = hash; Process.spawn_async_with_pipes ("/tmp", tor_argv, null, SpawnFlags.SEARCH_PATH, null, out tor_pid, null, out tor_stdout); var channel = new IOChannel.unix_new (tor_stdout); channel.add_watch (IOCondition.IN | IOCondition.PRI | IOCondition.ERR | IOCondition.HUP | IOCondition.NVAL, tor_io_func); } if (polipo_pid == (Pid) 0) { Process.spawn_async_with_pipes ("/tmp", { "/usr/bin/polipo" }, null, SpawnFlags.SEARCH_PATH, null, out polipo_pid); } /* --> proxy settings and will be set up and tor_connected will * be set to true once Tor signals 100% */ } catch (SpawnError e) { Hildon.Banner.show_information (null, null, "DEBUG: Failed to spawn polipo and tor: %s".printf (e.message)); return; } tor_log = ""; if (log_label != null) log_label.label = tor_log; update_status (); } /** * Stop Tor and revert proxy settings */ private void stop_tor () { proxy_restore (); tor_connected = false; if (polipo_pid != (Pid) 0) { Process.close_pid (polipo_pid); Posix.kill ((Posix.pid_t) polipo_pid, Posix.SIGKILL); polipo_pid = (Pid) 0; } if (tor_pid != (Pid) 0) { Process.close_pid (tor_pid); Posix.kill ((Posix.pid_t) tor_pid, Posix.SIGKILL); tor_pid = (Pid) 0; } update_status (); } /** * Setup proxy settings to route through the Tor network */ private void proxy_setup () { if (backup == null) try { backup = new ProxyBackup (); backup.use_http_proxy = gconf.get_bool (GCONF_KEY_PROXY_HTTP_ENABLED); backup.http_host = gconf.get_string (GCONF_KEY_PROXY_HTTP_HOST); backup.socks_host = gconf.get_string (GCONF_KEY_PROXY_SOCKS_HOST); backup.secure_host = gconf.get_string (GCONF_KEY_PROXY_SECURE_HOST); backup.http_port = gconf.get_int (GCONF_KEY_PROXY_HTTP_PORT); backup.socks_port = gconf.get_int (GCONF_KEY_PROXY_SOCKS_PORT); backup.secure_port = gconf.get_int (GCONF_KEY_PROXY_SECURE_PORT); backup.mode = gconf.get_string (GCONF_KEY_PROXY_MODE); } catch (Error e) { critical ("Error saving proxy settings: %s", e.message); backup = new ProxyBackup (); backup.use_http_proxy = false; backup.http_host = ""; backup.socks_host = ""; backup.secure_host = ""; backup.http_port = 8080; backup.socks_port = 0; backup.secure_port = 0; backup.mode = "none"; } try { // Hildon.Banner.show_information (null, null, "DEBUG: Proxy setup"); gconf.set_bool (GCONF_KEY_PROXY_HTTP_ENABLED, true); gconf.set_string (GCONF_KEY_PROXY_HTTP_HOST, "127.0.0.1"); gconf.set_string (GCONF_KEY_PROXY_SOCKS_HOST, "127.0.0.1"); gconf.set_string (GCONF_KEY_PROXY_SECURE_HOST, "127.0.0.1"); gconf.set_int (GCONF_KEY_PROXY_HTTP_PORT, 8118); gconf.set_int (GCONF_KEY_PROXY_SOCKS_PORT, 9050); gconf.set_int (GCONF_KEY_PROXY_SECURE_PORT, 8118); gconf.set_string (GCONF_KEY_PROXY_MODE, "manual"); } catch (Error e) { critical ("Error changing proxy settings: %s", e.message); } } /** * Revert proxy settings */ private void proxy_restore () { if (backup != null) try { // Hildon.Banner.show_information (null, null, "DEBUG: Restoring proxy settings"); gconf.set_bool (GCONF_KEY_PROXY_HTTP_ENABLED, backup.use_http_proxy); gconf.set_string (GCONF_KEY_PROXY_HTTP_HOST, backup.http_host); gconf.set_string (GCONF_KEY_PROXY_SOCKS_HOST, backup.socks_host); gconf.set_string (GCONF_KEY_PROXY_SECURE_HOST, backup.secure_host); gconf.set_int (GCONF_KEY_PROXY_HTTP_PORT, backup.http_port); gconf.set_int (GCONF_KEY_PROXY_SOCKS_PORT, backup.socks_port); gconf.set_int (GCONF_KEY_PROXY_SECURE_PORT, backup.secure_port); gconf.set_string (GCONF_KEY_PROXY_MODE, backup.mode); backup = null; } catch (Error e) { critical ("Error restoring proxy: %s", e.message); } } /** * Show the bridge relay configuration dialog */ private void bridges_clicked_cb () { var dialog = new BridgeDialog (); dialog.show (); } /** * Show the exit node configuration dialog */ private void exit_nodes_clicked_cb () { var dialog = new ExitNodeDialog (tor_control); dialog.show (); } /** * Check whether the IP address consists of four numbers in the 0..255 range */ bool is_valid_ip_address (string address) { string[] ip = address.split ("."); if (ip.length != 4) return false; for (int i = 0; i < ip.length; i++) { int n = ip[i].to_int (); if (n < 0 || n > 255) return false; } return true; } /** * Show the Tor log dialog */ private void show_tor_log () { var dialog = new Gtk.Dialog (); var content = (Gtk.VBox) dialog.get_content_area (); content.set_size_request (-1, 5*70); dialog.set_title (_("Log")); var pannable = new Hildon.PannableArea (); pannable.mov_mode = Hildon.MovementMode.BOTH; log_label = new Gtk.Label (tor_log); log_label.set_alignment (0, 0); pannable.add_with_viewport (log_label); content.pack_start (pannable, true, true, 0); dialog.response.connect (() => { log_label = null; }); dialog.show_all (); } /** * Callback for the status menu button clicked signal */ private const int RESPONSE_LOG = 1; private void button_clicked_cb () { var dialog = new Gtk.Dialog (); var content = (Gtk.VBox) dialog.get_content_area (); content.set_size_request (-1, 3*70); dialog.set_title (_("Tor: anonymity online")); var check = new Hildon.CheckButton (Hildon.SizeType.FINGER_HEIGHT); check.set_label (_("Enable onion routing")); check.set_active (tor_enabled); content.pack_start (check, true, true, 0); var button = new Hildon.Button.with_text (Hildon.SizeType.FINGER_HEIGHT, Hildon.ButtonArrangement.VERTICAL, _("Bridge relays"), get_bridge_list ()); button.set_style (Hildon.ButtonStyle.PICKER); button.set_alignment (0, 0.5f, 0, 0.5f); button.clicked.connect (bridges_clicked_cb); content.pack_start (button, true, true, 0); button = new Hildon.Button.with_text (Hildon.SizeType.FINGER_HEIGHT, Hildon.ButtonArrangement.VERTICAL, _("Restrict exit nodes"), get_exit_node_list ()); button.set_style (Hildon.ButtonStyle.PICKER); button.set_alignment (0, 0.5f, 0, 0.5f); button.clicked.connect (exit_nodes_clicked_cb); content.pack_start (button, true, true, 0); dialog.add_button (_("Log"), RESPONSE_LOG); dialog.add_button (_("Save"), Gtk.ResponseType.ACCEPT); dialog.response.connect ((response_id) => { if (response_id == RESPONSE_LOG) { show_tor_log (); return; } if (response_id == Gtk.ResponseType.ACCEPT) { if (!tor_enabled && check.get_active ()) try { gconf.set_bool (GCONF_KEY_TOR_ENABLED, true); // Enabled by user interaction, so connect if needed if (!conic_connected) conic.connect (ConIc.ConnectFlags.NONE); } catch (Error e) { Hildon.Banner.show_information (null, null, "Failed to enable GConf key"); } else if (tor_enabled && !check.get_active ()) try { gconf.set_bool (GCONF_KEY_TOR_ENABLED, false); } catch (Error e) { Hildon.Banner.show_information (null, null, "Failed to disable GConf key"); } } dialog.destroy (); }); dialog.show_all (); } private string get_bridge_list () { string list = null; var bridges = new SList (); try { bridges = gconf.get_list (GCONF_KEY_BRIDGES, GConf.ValueType.STRING); } catch (Error e) { critical ("Error loading bridges: %s", e.message); } foreach (string bridge in bridges) { if (list == null) list = bridge; else list += ", " + bridge; } if (list == null) list = _("None"); return list; } private string get_exit_node_list () { string list = null; var exits = new SList (); try { exits = gconf.get_list (GCONF_KEY_EXITNODES, GConf.ValueType.STRING); } catch (Error e) { error ("Error loading exit nodes: %s", e.message); } foreach (string exit in exits) { if (list == null) list = exit; else list += ", " + exit; } if (list == null) list = _("None"); return list; } /** * Callback for GConf change notification on the tor_enabled key */ private void tor_enabled_changed_cb (GConf.Client gc, uint cxnid, GConf.Entry entry) { if (entry.key == GCONF_KEY_TOR_ENABLED) { bool old_tor_enabled = tor_enabled; tor_enabled = entry.get_value ().get_bool (); if (old_tor_enabled == tor_enabled) return; if (tor_enabled) { // Start Tor immediately if a connection is already available if (conic_connected) start_tor (); } else { stop_tor (); if (conic_connected) conic.disconnect (); } } } /** * Callback for the ConIc connection-event signal */ private void conic_connection_event_cb (ConIc.Connection conic, ConIc.ConnectionEvent event) { var status = event.get_status (); switch (status) { case ConIc.ConnectionStatus.CONNECTED: conic_connected = true; if (tor_enabled) { start_tor (); } else { update_status (); } break; case ConIc.ConnectionStatus.DISCONNECTING: conic_connected = false; stop_tor (); break; case ConIc.ConnectionStatus.DISCONNECTED: case ConIc.ConnectionStatus.NETWORK_UP: // ignore break; } var error = event.get_error (); switch (error) { case ConIc.ConnectionError.CONNECTION_FAILED: Hildon.Banner.show_information (null, null, "DEBUG: ConIc connection failed"); break; case ConIc.ConnectionError.USER_CANCELED: Hildon.Banner.show_information (null, null, "DEBUG: ConIc user canceled"); break; case ConIc.ConnectionError.NONE: case ConIc.ConnectionError.INVALID_IAP: // ignore break; } } private void create_widgets () { // Status menu button button = new Hildon.Button.with_text (Hildon.SizeType.FINGER_HEIGHT, Hildon.ButtonArrangement.VERTICAL, _("The Onion Router"), tor_enabled ? _("Enabled") : _("Disabled")); button.set_alignment (0.0f, 0.5f, 1.0f, 1.0f); button.set_style (Hildon.ButtonStyle.PICKER); button.clicked.connect (button_clicked_cb); add (button); log_label = null; // Status area icon update_status (); show_all (); } construct { // Gettext hook-up Intl.setlocale (LocaleCategory.ALL, ""); Intl.bindtextdomain (Config.GETTEXT_PACKAGE, Config.LOCALEDIR); Intl.textdomain (Config.GETTEXT_PACKAGE); // GConf hook-up gconf = GConf.Client.get_default (); try { tor_enabled = gconf.get_bool (GCONF_KEY_TOR_ENABLED); // Request change notifications for the tor_enabled key gconf.add_dir (GCONF_DIR_TOR, GConf.ClientPreloadType.ONELEVEL); gconf.notify_add (GCONF_KEY_TOR_ENABLED, tor_enabled_changed_cb); } catch (Error e) { critical ("Failed to get GConf setting: %s", e.message); } tor_connected = false; // ConIc hook-up conic = new ConIc.Connection (); if (conic == null) { Hildon.Banner.show_information (null, null, "DEBUG: ConIc hook-up failed"); } conic_connected = false; conic.automatic_connection_events = true; if (tor_enabled) conic.connect (ConIc.ConnectFlags.AUTOMATICALLY_TRIGGERED); conic.connection_event.connect (conic_connection_event_cb); // Osso hook-up osso = new Osso.Context (STATUSMENU_TOR_LIBOSSO_SERVICE_NAME, Config.VERSION, true, null); create_widgets (); } } /** * Vala code can't use the HD_DEFINE_PLUGIN_MODULE macro, but it handles * most of the class registration issues itself. Only this code from * HD_PLUGIN_MODULE_SYMBOLS_CODE has to be has to be included manually * to register with hildon-desktop: */ [ModuleInit] public void hd_plugin_module_load (TypeModule plugin) { // [ModuleInit] registers types automatically ((HD.PluginModule) plugin).add_type (typeof (TorStatusMenuItem)); } public void hd_plugin_module_unload (HD.PluginModule plugin) { }