1 /* This file is part of status-area-applet-tor.
3 * Copyright (C) 2010 Philipp Zabel
5 * status-area-applet-tor is free software: you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License as published
7 * by the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
10 * status-area-applet-tor is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with status-area-applet-tor. If not, see <http://www.gnu.org/licenses/>.
21 public bool use_http_proxy;
22 public string http_host;
23 public string socks_host;
24 public string secure_host;
26 public int socks_port;
27 public int secure_port;
31 class TorStatusMenuItem : HD.StatusMenuItem {
32 private const string STATUSMENU_TOR_LIBOSSO_SERVICE_NAME = "tor_status_menu_item";
34 private const int STATUS_MENU_ICON_SIZE = 48;
35 private const int STATUS_AREA_ICON_SIZE = 18;
37 private const string GCONF_DIR_TOR = "/apps/maemo/tor";
38 private const string GCONF_KEY_TOR_ENABLED = GCONF_DIR_TOR + "/enabled";
39 private const string GCONF_KEY_BRIDGES = GCONF_DIR_TOR + "/bridges";
41 private const string GCONF_DIR_PROXY_HTTP = "/system/http_proxy";
42 private const string GCONF_KEY_PROXY_HTTP_ENABLED = GCONF_DIR_PROXY_HTTP + "/use_http_proxy";
43 private const string GCONF_KEY_PROXY_HTTP_HOST = GCONF_DIR_PROXY_HTTP + "/host";
44 private const string GCONF_KEY_PROXY_HTTP_PORT = GCONF_DIR_PROXY_HTTP + "/port";
46 private const string GCONF_DIR_PROXY = "/system/proxy";
47 private const string GCONF_KEY_PROXY_MODE = GCONF_DIR_PROXY + "/mode";
48 private const string GCONF_KEY_PROXY_SOCKS_HOST = GCONF_DIR_PROXY + "/socks_host";
49 private const string GCONF_KEY_PROXY_SOCKS_PORT = GCONF_DIR_PROXY + "/socks_port";
50 private const string GCONF_KEY_PROXY_SECURE_HOST = GCONF_DIR_PROXY + "/secure_host";
51 private const string GCONF_KEY_PROXY_SECURE_PORT = GCONF_DIR_PROXY + "/secure_port";
57 Gdk.Pixbuf icon_connecting;
58 Gdk.Pixbuf icon_connected;
59 Gtk.Image icon_enabled;
60 Gtk.Image icon_disabled;
62 // ConIc, GConf and Osso context
65 ConIc.Connection conic;
76 TorControl.Connection tor_control;
80 * Update status area icon and status menu button value
82 private void update_status () {
83 if (tor_enabled && tor_connected && icon_connected == null) try {
84 var icon_theme = Gtk.IconTheme.get_default ();
85 var pixbuf = icon_theme.load_icon ("statusarea_tor_connected",
86 STATUS_AREA_ICON_SIZE,
87 Gtk.IconLookupFlags.NO_SVG);
88 icon_connected = pixbuf;
92 if (tor_enabled && !tor_connected && icon_connecting == null) try {
93 var icon_theme = Gtk.IconTheme.get_default ();
94 var pixbuf = icon_theme.load_icon ("statusarea_tor_connecting",
95 STATUS_AREA_ICON_SIZE,
96 Gtk.IconLookupFlags.NO_SVG);
97 icon_connecting = pixbuf;
101 if (tor_enabled && icon_enabled == null) try {
102 var icon_theme = Gtk.IconTheme.get_default();
103 var pixbuf = icon_theme.load_icon ("statusarea_tor_enabled",
104 STATUS_MENU_ICON_SIZE,
105 Gtk.IconLookupFlags.NO_SVG);
106 icon_enabled = new Gtk.Image.from_pixbuf (pixbuf);
110 if (!tor_enabled && icon_disabled == null) try {
111 var icon_theme = Gtk.IconTheme.get_default();
112 var pixbuf = icon_theme.load_icon ("statusarea_tor_disabled",
113 STATUS_MENU_ICON_SIZE,
114 Gtk.IconLookupFlags.NO_SVG);
115 icon_disabled = new Gtk.Image.from_pixbuf (pixbuf);
120 if (conic_connected && tor_enabled) {
121 set_status_area_icon (tor_connected ? icon_connected : icon_connecting);
122 button.set_value (tor_connected ? _("Connected") : _("Connecting ..."));
124 set_status_area_icon (null);
125 button.set_value (tor_enabled ? _("Disconnected") : _("Disabled"));
127 button.set_image (tor_enabled ? icon_enabled : icon_disabled);
131 * Callback for Tor daemon line output
133 private bool tor_io_func (IOChannel source, IOCondition condition) {
135 if ((condition & (IOCondition.IN | IOCondition.PRI)) != 0) {
139 /* var status = */ source.read_line (out line, out length, null);
142 if ("[notice]" in line) {
143 if ("Bootstrapped 100%" in line) {
144 tor_connected = true;
148 if ("Opening Control listener on 127.0.0.1:9051" in line) {
149 tor_control = new TorControl.Connection ();
150 tor_control_auth.begin ();
154 Hildon.Banner.show_information (null, null, "DEBUG: %s".printf (line));
158 Hildon.Banner.show_information (null, null, "Error: %s".printf (e.message));
161 if ((condition & (IOCondition.ERR | IOCondition.HUP | IOCondition.NVAL)) != 0) {
168 * Authenticate with Tor on the control channel
170 private async void tor_control_auth () throws Error {
171 yield tor_control.authenticate_async (password);
173 var bridges = new SList<string> ();
175 bridges = gconf.get_list (GCONF_KEY_BRIDGES, GConf.ValueType.STRING);
177 error ("Error loading bridges: %s", e.message);
181 if (bridges.length () <= 0)
184 // Enable bridge relays
185 tor_control.set_conf_list ("Bridge", bridges);
186 tor_control.set_conf_bool ("UseBridges", true);
188 bool use = yield tor_control.get_conf_bool_async ("UseBridges");
190 Hildon.Banner.show_information (null, null,
191 "Failed to set up bridge relays");
196 * Start Tor and setup proxy settings
198 private void start_tor () {
200 if (tor_pid == (Pid) 0) {
201 string[] tor_hash_argv = {
203 "--hash-password", "",
207 Random.set_seed ((uint32) tv.tv_usec);
208 password = "tor-status-%8x".printf (Random.next_int ());
209 tor_hash_argv[2] = password;
211 Process.spawn_sync ("/tmp", tor_hash_argv, null, 0, null, out hash);
212 hash = hash.str ("16:").replace ("\n", "");
215 Hildon.Banner.show_information (null, null,
216 "Failed to get hash");
220 string[] tor_argv = {
222 "--ControlPort", "9051",
223 "--HashedControlPassword", "",
227 Process.spawn_async_with_pipes ("/tmp",
230 SpawnFlags.SEARCH_PATH,
236 var channel = new IOChannel.unix_new (tor_stdout);
237 channel.add_watch (IOCondition.IN | IOCondition.PRI | IOCondition.ERR | IOCondition.HUP | IOCondition.NVAL, tor_io_func);
239 if (polipo_pid == (Pid) 0) {
240 Process.spawn_async_with_pipes ("/tmp",
241 { "/usr/bin/polipo" },
243 SpawnFlags.SEARCH_PATH,
248 /* --> proxy settings and will be set up and tor_connected will
249 * be set to true once Tor signals 100%
251 } catch (SpawnError e) {
252 Hildon.Banner.show_information (null, null, "DEBUG: Failed to spawn polipo and tor: %s".printf (e.message));
261 * Stop Tor and revert proxy settings
263 private void stop_tor () {
265 tor_connected = false;
266 if (polipo_pid != (Pid) 0) {
267 Process.close_pid (polipo_pid);
268 Posix.kill ((Posix.pid_t) polipo_pid, Posix.SIGKILL);
269 polipo_pid = (Pid) 0;
271 if (tor_pid != (Pid) 0) {
272 Process.close_pid (tor_pid);
273 Posix.kill ((Posix.pid_t) tor_pid, Posix.SIGKILL);
281 * Setup proxy settings to route through the Tor network
283 private void proxy_setup () {
284 if (backup == null) try {
285 backup = new ProxyBackup ();
286 backup.use_http_proxy = gconf.get_bool (GCONF_KEY_PROXY_HTTP_ENABLED);
288 backup.http_host = gconf.get_string (GCONF_KEY_PROXY_HTTP_HOST);
289 backup.socks_host = gconf.get_string (GCONF_KEY_PROXY_SOCKS_HOST);
290 backup.secure_host = gconf.get_string (GCONF_KEY_PROXY_SECURE_HOST);
291 backup.http_port = gconf.get_int (GCONF_KEY_PROXY_HTTP_PORT);
292 backup.socks_port = gconf.get_int (GCONF_KEY_PROXY_SOCKS_PORT);
293 backup.secure_port = gconf.get_int (GCONF_KEY_PROXY_SECURE_PORT);
295 backup.mode = gconf.get_string (GCONF_KEY_PROXY_MODE);
297 error ("Error saving proxy settings: %s", e.message);
298 backup = new ProxyBackup ();
299 backup.use_http_proxy = false;
301 backup.http_host = "";
302 backup.socks_host = "";
303 backup.secure_host = "";
304 backup.http_port = 8080;
305 backup.socks_port = 0;
306 backup.secure_port = 0;
308 backup.mode = "none";
311 // Hildon.Banner.show_information (null, null, "DEBUG: Proxy setup");
312 gconf.set_bool (GCONF_KEY_PROXY_HTTP_ENABLED, true);
314 gconf.set_string (GCONF_KEY_PROXY_HTTP_HOST, "127.0.0.1");
315 gconf.set_string (GCONF_KEY_PROXY_SOCKS_HOST, "127.0.0.1");
316 gconf.set_string (GCONF_KEY_PROXY_SECURE_HOST, "127.0.0.1");
317 gconf.set_int (GCONF_KEY_PROXY_HTTP_PORT, 8118);
318 gconf.set_int (GCONF_KEY_PROXY_SOCKS_PORT, 9050);
319 gconf.set_int (GCONF_KEY_PROXY_SECURE_PORT, 8118);
321 gconf.set_string (GCONF_KEY_PROXY_MODE, "manual");
323 error ("Error changing proxy settings: %s", e.message);
328 * Revert proxy settings
330 private void proxy_restore () {
331 if (backup != null) try {
332 // Hildon.Banner.show_information (null, null, "DEBUG: Restoring proxy settings");
333 gconf.set_bool (GCONF_KEY_PROXY_HTTP_ENABLED, backup.use_http_proxy);
335 gconf.set_string (GCONF_KEY_PROXY_HTTP_HOST, backup.http_host);
336 gconf.set_string (GCONF_KEY_PROXY_SOCKS_HOST, backup.socks_host);
337 gconf.set_string (GCONF_KEY_PROXY_SECURE_HOST, backup.secure_host);
338 gconf.set_int (GCONF_KEY_PROXY_HTTP_PORT, backup.http_port);
339 gconf.set_int (GCONF_KEY_PROXY_SOCKS_PORT, backup.socks_port);
340 gconf.set_int (GCONF_KEY_PROXY_SECURE_PORT, backup.secure_port);
342 gconf.set_string (GCONF_KEY_PROXY_MODE, backup.mode);
345 error ("Error restoring proxy: %s", e.message);
350 * Show the bridge relay configuration dialog
352 private const int RESPONSE_NEW = 1;
353 private void bridges_clicked_cb () {
354 var dialog = new Gtk.Dialog ();
355 var content = (Gtk.VBox) dialog.get_content_area ();
356 content.set_size_request (-1, 5*70);
358 dialog.set_title (_("Bridge relays"));
360 var bridges = new SList<string> ();
362 bridges = gconf.get_list (GCONF_KEY_BRIDGES, GConf.ValueType.STRING);
364 Hildon.Banner.show_information (null, null, "Error loading bridges: %s".printf (e.message));
367 var list_store = new Gtk.ListStore (1, typeof (string));
369 foreach (string bridge in bridges) {
370 list_store.append (out iter);
371 list_store.@set (iter, 0, bridge);
374 var pannable_area = new Hildon.PannableArea ();
375 var tree_view = new Gtk.TreeView.with_model (list_store);
376 var renderer = new Gtk.CellRendererText ();
377 var column = new Gtk.TreeViewColumn.with_attributes ("IP", renderer, "text", 0);
378 tree_view.append_column (column);
379 pannable_area.add (tree_view);
380 content.pack_start (pannable_area, true, true, 0);
382 tree_view.row_activated.connect ((path, column) => {
383 bridge_edit_dialog (list_store, path);
386 dialog.add_button (_("New"), RESPONSE_NEW);
387 dialog.response.connect ((response_id) => {
388 if (response_id == RESPONSE_NEW) {
389 bridge_edit_dialog (list_store, null);
397 * Show the bridge relay edit dialog
399 private const int RESPONSE_DELETE = 1;
400 private void bridge_edit_dialog (Gtk.ListStore store, Gtk.TreePath? path) {
401 var dialog = new Gtk.Dialog ();
402 var content = (Gtk.VBox) dialog.get_content_area ();
405 dialog.set_title (_("New bridge relay"));
407 dialog.set_title (_("Edit bridge relay"));
409 var size_group = new Gtk.SizeGroup (Gtk.SizeGroupMode.HORIZONTAL);
411 var hbox = new Gtk.HBox (false, Hildon.MARGIN_DOUBLE);
412 var label = new Gtk.Label (_("IP address"));
413 label.set_alignment (0, 0.5f);
414 size_group.add_widget (label);
415 hbox.pack_start (label, false, false, 0);
416 var ip_entry = new Hildon.Entry (Hildon.SizeType.FINGER_HEIGHT);
417 ip_entry.set ("hildon-input-mode", Hildon.GtkInputMode.NUMERIC |
418 Hildon.GtkInputMode.SPECIAL);
419 hbox.pack_start (ip_entry, true, true, 0);
420 content.pack_start (hbox, false, false, 0);
422 hbox = new Gtk.HBox (false, Hildon.MARGIN_DOUBLE);
423 label = new Gtk.Label (_("Port"));
424 label.set_alignment (0, 0.5f);
425 size_group.add_widget (label);
426 hbox.pack_start (label, false, false, 0);
427 var port_entry = new Hildon.Entry (Hildon.SizeType.FINGER_HEIGHT);
428 port_entry.set ("hildon-input-mode", Hildon.GtkInputMode.NUMERIC);
429 hbox.pack_start (port_entry, true, true, 0);
430 content.pack_start (hbox, true, true, 0);
432 hbox = new Gtk.HBox (false, Hildon.MARGIN_DOUBLE);
433 label = new Gtk.Label (_("Fingerprint"));
434 label.set_alignment (0, 0.5f);
435 size_group.add_widget (label);
436 hbox.pack_start (label, false, false, 0);
437 var fingerprint_entry = new Hildon.Entry (Hildon.SizeType.FINGER_HEIGHT);
438 fingerprint_entry.set ("hildon-input-mode", Hildon.GtkInputMode.HEXA);
439 hbox.pack_start (fingerprint_entry, true, true, 0);
440 content.pack_start (hbox, true, true, 0);
442 var iter = Gtk.TreeIter ();
444 port_entry.set_text ("443");
445 } else if (store.get_iter (out iter, path)) {
447 store.@get (iter, 0, out tmp);
448 string[] ip_port = tmp.split (":");
449 if (ip_port.length == 2) {
450 ip_entry.set_text (ip_port[0]);
451 port_entry.set_text (ip_port[1]);
454 dialog.add_button (_("Delete"), RESPONSE_DELETE);
456 dialog.add_button (_("Save"), Gtk.ResponseType.OK);
457 dialog.response.connect ((response_id) => {
458 var bridges = new SList<string> ();
460 if (response_id == RESPONSE_DELETE) {
464 if (store.get_iter_first (out iter)) do {
465 store.@get (iter, 0, out bridge);
466 bridges.append (bridge);
467 } while (store.iter_next (ref iter));
469 gconf.set_list (GCONF_KEY_BRIDGES,
470 GConf.ValueType.STRING,
473 Hildon.Banner.show_information (dialog, null,
474 "Failed to save bridge relay list: %s".printf (e.message));
479 if (response_id == Gtk.ResponseType.OK) {
480 if (!is_valid_ip_address (ip_entry.get_text ())) {
481 Hildon.Banner.show_information (dialog, null,
482 _("Invalid IP address"));
485 int port = port_entry.get_text ().to_int ();
486 if (port < 0 || port > 65565) {
487 Hildon.Banner.show_information (dialog, null,
488 _("Invalid port number"));
492 store.append (out iter);
494 store.@set (iter, 0, "%s:%d".printf (ip_entry.get_text (), port));
496 bridges = gconf.get_list (GCONF_KEY_BRIDGES,
497 GConf.ValueType.STRING);
499 Hildon.Banner.show_information (null, null,
500 "Error loading bridges: %s".printf (e.message));
503 bridges.append ("%s:%d".printf (ip_entry.get_text (), port));
507 if (store.get_iter_first (out iter)) do {
508 store.@get (iter, 0, out bridge);
509 bridges.append (bridge);
510 } while (store.iter_next (ref iter));
513 gconf.set_list (GCONF_KEY_BRIDGES,
514 GConf.ValueType.STRING,
517 Hildon.Banner.show_information (dialog, null,
518 "Failed to save bridge relay list: %s".printf (e.message));
529 * Check whether the IP address consists of four numbers in the 0..255 range
531 bool is_valid_ip_address (string address) {
532 string[] ip = address.split (".");
537 for (int i = 0; i < ip.length; i++) {
538 int n = ip[i].to_int ();
539 if (n < 0 || n > 255)
547 * Show the Tor log dialog
549 private void show_tor_log () {
550 var dialog = new Gtk.Dialog ();
551 var content = (Gtk.VBox) dialog.get_content_area ();
552 content.set_size_request (-1, 5*70);
554 dialog.set_title (_("Log"));
556 var pannable = new Hildon.PannableArea ();
557 var label = new Gtk.Label (tor_log);
558 pannable.add_with_viewport (label);
559 content.pack_start (pannable, true, true, 0);
565 * Callback for the status menu button clicked signal
567 private const int RESPONSE_LOG = 1;
568 private void button_clicked_cb () {
569 var dialog = new Gtk.Dialog ();
570 var content = (Gtk.VBox) dialog.get_content_area ();
571 content.set_size_request (-1, 2*70);
573 dialog.set_title (_("Tor: anonymity online"));
575 var check = new Hildon.CheckButton (Hildon.SizeType.FINGER_HEIGHT);
576 check.set_label (_("Enable onion routing"));
577 check.set_active (tor_enabled);
578 content.pack_start (check, true, true, 0);
580 var button = new Hildon.Button.with_text (Hildon.SizeType.FINGER_HEIGHT,
581 Hildon.ButtonArrangement.VERTICAL,
584 button.set_style (Hildon.ButtonStyle.PICKER);
585 button.set_alignment (0, 0.5f, 0, 0.5f);
586 button.clicked.connect (bridges_clicked_cb);
587 content.pack_start (button, true, true, 0);
589 dialog.add_button (_("Log"), RESPONSE_LOG);
591 dialog.add_button (_("Save"), Gtk.ResponseType.ACCEPT);
592 dialog.response.connect ((response_id) => {
593 if (response_id == RESPONSE_LOG) {
597 if (response_id == Gtk.ResponseType.ACCEPT) {
598 if (!tor_enabled && check.get_active ()) {
601 if (conic_connected) {
604 conic.connect (ConIc.ConnectFlags.NONE);
606 } else if (tor_enabled && !check.get_active ()) {
620 private string get_bridge_list () {
622 var bridges = new SList<string> ();
624 bridges = gconf.get_list (GCONF_KEY_BRIDGES, GConf.ValueType.STRING);
626 error ("Error loading bridges: %s", e.message);
628 foreach (string bridge in bridges) {
632 list += ", " + bridge;
641 * Callback for the ConIc connection-event signal
643 private void conic_connection_event_cb (ConIc.Connection conic, ConIc.ConnectionEvent event) {
644 var status = event.get_status ();
646 case ConIc.ConnectionStatus.CONNECTED:
647 conic_connected = true;
654 case ConIc.ConnectionStatus.DISCONNECTING:
655 conic_connected = false;
658 case ConIc.ConnectionStatus.DISCONNECTED:
659 case ConIc.ConnectionStatus.NETWORK_UP:
664 var error = event.get_error ();
666 case ConIc.ConnectionError.CONNECTION_FAILED:
667 Hildon.Banner.show_information (null, null, "DEBUG: ConIc connection failed");
669 case ConIc.ConnectionError.USER_CANCELED:
670 Hildon.Banner.show_information (null, null, "DEBUG: ConIc user canceled");
672 case ConIc.ConnectionError.NONE:
673 case ConIc.ConnectionError.INVALID_IAP:
679 private void create_widgets () {
680 // Status menu button
681 button = new Hildon.Button.with_text (Hildon.SizeType.FINGER_HEIGHT,
682 Hildon.ButtonArrangement.VERTICAL,
683 _("The Onion Router"),
684 tor_enabled ? _("Enabled") : _("Disabled"));
685 button.set_alignment (0.0f, 0.5f, 1.0f, 1.0f);
686 button.set_style (Hildon.ButtonStyle.PICKER);
687 button.clicked.connect (button_clicked_cb);
699 Intl.setlocale (LocaleCategory.ALL, "");
700 Intl.bindtextdomain (Config.GETTEXT_PACKAGE, Config.LOCALEDIR);
701 Intl.textdomain (Config.GETTEXT_PACKAGE);
704 gconf = GConf.Client.get_default ();
706 tor_enabled = gconf.get_bool (GCONF_KEY_TOR_ENABLED);
708 error ("Failed to get GConf setting: %s", e.message);
710 tor_connected = false;
713 conic = new ConIc.Connection ();
715 Hildon.Banner.show_information (null, null, "DEBUG: ConIc hook-up failed");
717 conic_connected = false;
718 conic.automatic_connection_events = true;
720 conic.connect (ConIc.ConnectFlags.AUTOMATICALLY_TRIGGERED);
721 conic.connection_event.connect (conic_connection_event_cb);
724 osso = new Osso.Context (STATUSMENU_TOR_LIBOSSO_SERVICE_NAME,
734 * Vala code can't use the HD_DEFINE_PLUGIN_MODULE macro, but it handles
735 * most of the class registration issues itself. Only this code from
736 * HD_PLUGIN_MODULE_SYMBOLS_CODE has to be has to be included manually
737 * to register with hildon-desktop:
740 public void hd_plugin_module_load (TypeModule plugin) {
741 // [ModuleInit] registers types automatically
742 ((HD.PluginModule) plugin).add_type (typeof (TorStatusMenuItem));
745 public void hd_plugin_module_unload (HD.PluginModule plugin) {