/* This file is part of status-area-applet-tor. * * Copyright (C) 2010 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_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; // 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 void update_status () { if (tor_enabled && tor_connected && icon_connected == null) try { 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; } catch (Error e) { error (e.message); } if (tor_enabled && !tor_connected && icon_connecting == null) try { 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; } catch (Error e) { error (e.message); } if (tor_enabled && icon_enabled == null) try { 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); } catch (Error e) { error (e.message); } if (!tor_enabled && icon_disabled == null) try { 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) { error (e.message); } 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); } /** * 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 ("[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 = new SList (); try { bridges = gconf.get_list (GCONF_KEY_BRIDGES, GConf.ValueType.STRING); } catch (Error e) { error ("Error loading bridges: %s", e.message); return; } if (bridges.length () <= 0) return; // 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"); } } /** * 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 ("16:").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 = ""; 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) { error ("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) { error ("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) { error ("Error restoring proxy: %s", e.message); } } /** * Show the bridge relay configuration dialog */ private const int RESPONSE_NEW = 1; private void bridges_clicked_cb () { var dialog = new Gtk.Dialog (); var content = (Gtk.VBox) dialog.get_content_area (); content.set_size_request (-1, 5*70); dialog.set_title (_("Bridge relays")); var bridges = new SList (); try { bridges = gconf.get_list (GCONF_KEY_BRIDGES, GConf.ValueType.STRING); } catch (Error e) { Hildon.Banner.show_information (null, null, "Error loading bridges: %s".printf (e.message)); } var list_store = new Gtk.ListStore (1, typeof (string)); Gtk.TreeIter iter; foreach (string bridge in bridges) { list_store.append (out iter); list_store.@set (iter, 0, bridge); } var pannable_area = new Hildon.PannableArea (); var tree_view = new Gtk.TreeView.with_model (list_store); var renderer = new Gtk.CellRendererText (); var column = new Gtk.TreeViewColumn.with_attributes ("IP", renderer, "text", 0); tree_view.append_column (column); pannable_area.add (tree_view); content.pack_start (pannable_area, true, true, 0); tree_view.row_activated.connect ((path, column) => { bridge_edit_dialog (list_store, path); }); dialog.add_button (_("New"), RESPONSE_NEW); dialog.response.connect ((response_id) => { if (response_id == RESPONSE_NEW) { bridge_edit_dialog (list_store, null); } }); dialog.show_all (); } /** * Show the bridge relay edit dialog */ private const int RESPONSE_DELETE = 1; private void bridge_edit_dialog (Gtk.ListStore store, Gtk.TreePath? path) { var dialog = new Gtk.Dialog (); var content = (Gtk.VBox) dialog.get_content_area (); if (path == null) dialog.set_title (_("New bridge relay")); else dialog.set_title (_("Edit bridge relay")); var size_group = new Gtk.SizeGroup (Gtk.SizeGroupMode.HORIZONTAL); var hbox = new Gtk.HBox (false, Hildon.MARGIN_DOUBLE); var label = new Gtk.Label (_("IP address")); label.set_alignment (0, 0.5f); size_group.add_widget (label); hbox.pack_start (label, false, false, 0); var ip_entry = new Hildon.Entry (Hildon.SizeType.FINGER_HEIGHT); ip_entry.set ("hildon-input-mode", Hildon.GtkInputMode.NUMERIC | Hildon.GtkInputMode.SPECIAL); hbox.pack_start (ip_entry, true, true, 0); content.pack_start (hbox, false, false, 0); hbox = new Gtk.HBox (false, Hildon.MARGIN_DOUBLE); label = new Gtk.Label (_("Port")); label.set_alignment (0, 0.5f); size_group.add_widget (label); hbox.pack_start (label, false, false, 0); var port_entry = new Hildon.Entry (Hildon.SizeType.FINGER_HEIGHT); port_entry.set ("hildon-input-mode", Hildon.GtkInputMode.NUMERIC); hbox.pack_start (port_entry, true, true, 0); content.pack_start (hbox, true, true, 0); hbox = new Gtk.HBox (false, Hildon.MARGIN_DOUBLE); label = new Gtk.Label (_("Fingerprint")); label.set_alignment (0, 0.5f); size_group.add_widget (label); hbox.pack_start (label, false, false, 0); var fingerprint_entry = new Hildon.Entry (Hildon.SizeType.FINGER_HEIGHT); fingerprint_entry.set ("hildon-input-mode", Hildon.GtkInputMode.HEXA); hbox.pack_start (fingerprint_entry, true, true, 0); content.pack_start (hbox, true, true, 0); var iter = Gtk.TreeIter (); if (path == null) { port_entry.set_text ("443"); } else if (store.get_iter (out iter, path)) { string tmp; store.@get (iter, 0, out tmp); string[] ip_port = tmp.split (":"); if (ip_port.length == 2) { ip_entry.set_text (ip_port[0]); port_entry.set_text (ip_port[1]); } dialog.add_button (_("Delete"), RESPONSE_DELETE); } dialog.add_button (_("Save"), Gtk.ResponseType.OK); dialog.response.connect ((response_id) => { var bridges = new SList (); if (response_id == RESPONSE_DELETE) { if (path != null) { store.remove (iter); string bridge; if (store.get_iter_first (out iter)) do { store.@get (iter, 0, out bridge); bridges.append (bridge); } while (store.iter_next (ref iter)); try { gconf.set_list (GCONF_KEY_BRIDGES, GConf.ValueType.STRING, bridges); } catch (Error e) { Hildon.Banner.show_information (dialog, null, "Failed to save bridge relay list: %s".printf (e.message)); } } dialog.destroy (); } if (response_id == Gtk.ResponseType.OK) { if (!is_valid_ip_address (ip_entry.get_text ())) { Hildon.Banner.show_information (dialog, null, _("Invalid IP address")); return; } int port = port_entry.get_text ().to_int (); if (port < 0 || port > 65565) { Hildon.Banner.show_information (dialog, null, _("Invalid port number")); return; } if (path == null) { store.append (out iter); } store.@set (iter, 0, "%s:%d".printf (ip_entry.get_text (), port)); try { bridges = gconf.get_list (GCONF_KEY_BRIDGES, GConf.ValueType.STRING); } catch (Error e) { Hildon.Banner.show_information (null, null, "Error loading bridges: %s".printf (e.message)); } if (path == null) { bridges.append ("%s:%d".printf (ip_entry.get_text (), port)); } else { bridges = null; string bridge; if (store.get_iter_first (out iter)) do { store.@get (iter, 0, out bridge); bridges.append (bridge); } while (store.iter_next (ref iter)); } try { gconf.set_list (GCONF_KEY_BRIDGES, GConf.ValueType.STRING, bridges); } catch (Error e) { Hildon.Banner.show_information (dialog, null, "Failed to save bridge relay list: %s".printf (e.message)); } dialog.destroy (); } }); dialog.show_all (); } /** * 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; var label = new Gtk.Label (tor_log); pannable.add_with_viewport (label); content.pack_start (pannable, true, true, 0); 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, 2*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); 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 ()) { tor_enabled = true; if (conic_connected) { start_tor (); } else { conic.connect (ConIc.ConnectFlags.NONE); } } else if (tor_enabled && !check.get_active ()) { tor_enabled = false; stop_tor (); if (conic_connected) conic.disconnect (); } } 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) { error ("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; } /** * 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); // 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); } catch (Error e) { error ("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) { }