Make log viewer pannable in both directions
[tor-status] / src / status-area-applet-tor.vala
index 7647239..dc7e35e 100644 (file)
@@ -36,6 +36,7 @@ class TorStatusMenuItem : HD.StatusMenuItem {
 
        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";
@@ -53,9 +54,10 @@ class TorStatusMenuItem : HD.StatusMenuItem {
        Hildon.Button button;
 
        // Icons
-       Gdk.Pixbuf icon_disabled;
        Gdk.Pixbuf icon_connecting;
        Gdk.Pixbuf icon_connected;
+       Gtk.Image icon_enabled;
+       Gtk.Image icon_disabled;
 
        // ConIc, GConf and Osso context
        Osso.Context osso;
@@ -70,50 +72,59 @@ class TorStatusMenuItem : HD.StatusMenuItem {
        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 () {
-               Gtk.IconTheme icon_theme;
-               Gdk.Pixbuf pixbuf;
-
                if (tor_enabled && tor_connected && icon_connected == null) try {
-                       icon_theme = Gtk.IconTheme.get_default ();
-                       pixbuf = icon_theme.load_icon ("tor_status_connected",
-                                                      STATUS_AREA_ICON_SIZE,
-                                                      Gtk.IconLookupFlags.NO_SVG);
+                       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 && icon_disabled == null) try {
-                       icon_theme = Gtk.IconTheme.get_default ();
-                       pixbuf = icon_theme.load_icon ("tor_status_disabled",
-                                                      STATUS_AREA_ICON_SIZE,
-                                                      Gtk.IconLookupFlags.NO_SVG);
-                       icon_disabled = pixbuf;
+               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 && !tor_connected && icon_connecting == null) try {
-                       icon_theme = Gtk.IconTheme.get_default ();
-                       pixbuf = icon_theme.load_icon ("tor_status_connecting",
-                                                      STATUS_AREA_ICON_SIZE,
-                                                      Gtk.IconLookupFlags.NO_SVG);
-                       icon_connecting = pixbuf;
+               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) {
-                       pixbuf = tor_connected ? icon_connected : icon_connecting;
-                       button.set_value (tor_connected ? "Connected" : "Connecting ...");
+                       set_status_area_icon (tor_connected ? icon_connected : icon_connecting);
+                       button.set_value (tor_connected ? _("Connected") : _("Connecting ..."));
                } else {
-                       pixbuf = conic_connected ? icon_disabled : null;
-                       button.set_value (tor_enabled ? "Disconnected" : "Disabled");
+                       set_status_area_icon (null);
+                       button.set_value (tor_enabled ? _("Disconnected") : _("Disabled"));
                }
-               set_status_area_icon (pixbuf);
+               button.set_image (tor_enabled ? icon_enabled : icon_disabled);
        }
 
        /**
@@ -127,12 +138,17 @@ class TorStatusMenuItem : HD.StatusMenuItem {
                        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));
@@ -149,13 +165,67 @@ class TorStatusMenuItem : HD.StatusMenuItem {
        }
 
        /**
+        * 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<string> ();
+               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",
-                                                               { "/usr/sbin/tor" },
+                                                               tor_argv,
                                                                null,
                                                                SpawnFlags.SEARCH_PATH,
                                                                null,
@@ -179,10 +249,11 @@ class TorStatusMenuItem : HD.StatusMenuItem {
                         * be set to true once Tor signals 100%
                         */
                } catch (SpawnError e) {
-                       error ("Failed to spawn polipo and tor: %s", e.message);
+                       Hildon.Banner.show_information (null, null, "DEBUG: Failed to spawn polipo and tor: %s".printf (e.message));
                        return;
                }
 
+               tor_log = "";
                update_status ();
        }
 
@@ -276,21 +347,254 @@ class TorStatusMenuItem : HD.StatusMenuItem {
        }
 
        /**
+        * 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<string> ();
+               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<string> ();
+
+                       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 ("The Onion Router");
+               dialog.set_title (_("Tor: anonymity online"));
 
                var check = new Hildon.CheckButton (Hildon.SizeType.FINGER_HEIGHT);
-               check.set_label ("Enable onion routing");
+               check.set_label (_("Enable onion routing"));
                check.set_active (tor_enabled);
                content.pack_start (check, true, true, 0);
 
-               dialog.add_button ("Save", Gtk.ResponseType.ACCEPT);
+               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;
@@ -314,6 +618,26 @@ class TorStatusMenuItem : HD.StatusMenuItem {
                dialog.show_all ();
        }
 
+       private string get_bridge_list () {
+               string list = null;
+               var bridges = new SList<string> ();
+               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
         */
@@ -354,26 +678,13 @@ class TorStatusMenuItem : HD.StatusMenuItem {
        }
 
        private void create_widgets () {
-               Gtk.IconTheme icon_theme;
-               Gdk.Pixbuf pixbuf;
-               Gtk.Image image;
-
                // Status menu button
                button = new Hildon.Button.with_text (Hildon.SizeType.FINGER_HEIGHT,
                                                      Hildon.ButtonArrangement.VERTICAL,
-                                                     "The Onion Router",
-                                                     tor_enabled ? "Enabled" : "Disabled");
-               icon_theme = Gtk.IconTheme.get_default();
-               try {
-                       pixbuf = icon_theme.load_icon ("tor_onion",
-                                                      STATUS_MENU_ICON_SIZE,
-                                                      Gtk.IconLookupFlags.NO_SVG);
-                       image = new Gtk.Image.from_pixbuf (pixbuf);
-                       button.set_image (image);
-               } catch (Error e) {
-                       error (e.message);
-               }
+                                                     _("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);
@@ -385,6 +696,11 @@ class TorStatusMenuItem : HD.StatusMenuItem {
        }
 
        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 {