Debian packaging: 0.0.5-1
[tor-status] / src / status-area-applet-tor.vala
1 /* This file is part of status-area-applet-tor.
2  *
3  * Copyright (C) 2010 Philipp Zabel
4  *
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.
9  *
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.
14  *
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/>.
17  */
18
19 [Compact]
20 class ProxyBackup {
21         public bool use_http_proxy;
22         public string http_host;
23         public string socks_host;
24         public string secure_host;
25         public int http_port;
26         public int socks_port;
27         public int secure_port;
28         public string mode;
29 }
30
31 class TorStatusMenuItem : HD.StatusMenuItem {
32         private const string STATUSMENU_TOR_LIBOSSO_SERVICE_NAME = "tor_status_menu_item";
33
34         private const int STATUS_MENU_ICON_SIZE = 48;
35         private const int STATUS_AREA_ICON_SIZE = 18;
36
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";
40
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";
45
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";
52
53         // Widgets
54         Hildon.Button button;
55         Gtk.Label log_label;
56
57         // Icons
58         Gdk.Pixbuf icon_connecting;
59         Gdk.Pixbuf icon_connected;
60         Gtk.Image icon_enabled;
61         Gtk.Image icon_disabled;
62
63         // ConIc, GConf and Osso context
64         Osso.Context osso;
65         GConf.Client gconf;
66         ConIc.Connection conic;
67         bool conic_connected;
68
69         // Internal state
70         bool tor_enabled;
71         bool tor_connected;
72         Pid tor_pid;
73         int tor_stdout;
74         Pid polipo_pid;
75         ProxyBackup backup;
76         string tor_log;
77         TorControl.Connection tor_control;
78         string password;
79
80         /**
81          * Update status area icon and status menu button value
82          */
83         private void update_status () {
84                 if (tor_enabled && tor_connected && icon_connected == null) try {
85                         var icon_theme = Gtk.IconTheme.get_default ();
86                         var pixbuf = icon_theme.load_icon ("statusarea_tor_connected",
87                                                            STATUS_AREA_ICON_SIZE,
88                                                            Gtk.IconLookupFlags.NO_SVG);
89                         icon_connected = pixbuf;
90                 } catch (Error e) {
91                         error (e.message);
92                 }
93                 if (tor_enabled && !tor_connected && icon_connecting == null) try {
94                         var icon_theme = Gtk.IconTheme.get_default ();
95                         var pixbuf = icon_theme.load_icon ("statusarea_tor_connecting",
96                                                            STATUS_AREA_ICON_SIZE,
97                                                            Gtk.IconLookupFlags.NO_SVG);
98                         icon_connecting = pixbuf;
99                 } catch (Error e) {
100                         error (e.message);
101                 }
102                 if (tor_enabled && icon_enabled == null) try {
103                         var icon_theme = Gtk.IconTheme.get_default();
104                         var pixbuf = icon_theme.load_icon ("statusarea_tor_enabled",
105                                                            STATUS_MENU_ICON_SIZE,
106                                                            Gtk.IconLookupFlags.NO_SVG);
107                         icon_enabled = new Gtk.Image.from_pixbuf (pixbuf);
108                 } catch (Error e) {
109                         error (e.message);
110                 }
111                 if (!tor_enabled && icon_disabled == null) try {
112                         var icon_theme = Gtk.IconTheme.get_default();
113                         var pixbuf = icon_theme.load_icon ("statusarea_tor_disabled",
114                                                            STATUS_MENU_ICON_SIZE,
115                                                            Gtk.IconLookupFlags.NO_SVG);
116                         icon_disabled = new Gtk.Image.from_pixbuf (pixbuf);
117                 } catch (Error e) {
118                         error (e.message);
119                 }
120
121                 if (conic_connected && tor_enabled) {
122                         set_status_area_icon (tor_connected ? icon_connected : icon_connecting);
123                         button.set_value (tor_connected ? _("Connected") : _("Connecting ..."));
124                 } else {
125                         set_status_area_icon (null);
126                         button.set_value (tor_enabled ? _("Disconnected") : _("Disabled"));
127                 }
128                 button.set_image (tor_enabled ? icon_enabled : icon_disabled);
129         }
130
131         /**
132          * Callback for Tor daemon line output
133          */
134         private bool tor_io_func (IOChannel source, IOCondition condition) {
135
136                 if ((condition & (IOCondition.IN | IOCondition.PRI)) != 0) {
137                         string line = null;
138                         size_t length;
139                         try {
140                                 /* var status = */ source.read_line (out line, out length, null);
141
142                                 tor_log += line;
143                                 if (log_label != null)
144                                         log_label.label = tor_log;
145
146                                 if ("[notice]" in line) {
147                                         if ("Bootstrapped 100%" in line) {
148                                                 tor_connected = true;
149                                                 proxy_setup ();
150                                                 update_status ();
151                                         }
152                                         if ("Opening Control listener on 127.0.0.1:9051" in line) {
153                                                 tor_control = new TorControl.Connection ();
154                                                 tor_control_auth.begin ();
155                                         }
156                                 } else {
157                                         // FIXME
158                                         Hildon.Banner.show_information (null, null, "DEBUG: %s".printf (line));
159                                 }
160                         } catch (Error e) {
161                                 // FIXME
162                                 Hildon.Banner.show_information (null, null, "Error: %s".printf (e.message));
163                         }
164                 }
165                 if ((condition & (IOCondition.ERR | IOCondition.HUP | IOCondition.NVAL)) != 0) {
166                         return false;
167                 }
168                 return true;
169         }
170
171         /**
172          * Authenticate with Tor on the control channel
173          */
174         private async void tor_control_auth () throws Error {
175                 yield tor_control.authenticate_async (password);
176
177                 try {
178                         var bridges = gconf.get_list (GCONF_KEY_BRIDGES, GConf.ValueType.STRING);
179
180                         if (bridges.length () > 0) {
181                                 // Enable bridge relays
182                                 tor_control.set_conf_list ("Bridge", bridges);
183                                 tor_control.set_conf_bool ("UseBridges", true);
184
185                                 bool use = yield tor_control.get_conf_bool_async ("UseBridges");
186                                 if (!use) {
187                                         Hildon.Banner.show_information (null, null,
188                                                                         "Failed to set up bridge relays");
189                                 }
190                         }
191                 } catch (Error e) {
192                         error ("Error loading bridges: %s", e.message);
193                 }
194         }
195
196         /**
197          * Start Tor and setup proxy settings
198          */
199         private void start_tor () {
200                 try {
201                         if (tor_pid == (Pid) 0) {
202                                 string[] tor_hash_argv = {
203                                         "/usr/sbin/tor",
204                                         "--hash-password", "",
205                                         null
206                                 };
207                                 var tv = TimeVal ();
208                                 Random.set_seed ((uint32) tv.tv_usec);
209                                 password = "tor-status-%8x".printf (Random.next_int ());
210                                 tor_hash_argv[2] = password;
211                                 string hash;
212                                 Process.spawn_sync ("/tmp", tor_hash_argv, null, 0, null, out hash);
213                                 hash = hash.str ("16:").replace ("\n", "");
214
215                                 if (hash == null) {
216                                         Hildon.Banner.show_information (null, null,
217                                                                         "Failed to get hash");
218                                         return;
219                                 }
220
221                                 string[] tor_argv = {
222                                         "/usr/sbin/tor",
223                                         "--ControlPort", "9051",
224                                         "--HashedControlPassword", "",
225                                         null
226                                 };
227                                 tor_argv[4] = hash;
228                                 Process.spawn_async_with_pipes ("/tmp",
229                                                                 tor_argv,
230                                                                 null,
231                                                                 SpawnFlags.SEARCH_PATH,
232                                                                 null,
233                                                                 out tor_pid,
234                                                                 null,
235                                                                 out tor_stdout);
236
237                                 var channel = new IOChannel.unix_new (tor_stdout);
238                                 channel.add_watch (IOCondition.IN | IOCondition.PRI | IOCondition.ERR | IOCondition.HUP | IOCondition.NVAL, tor_io_func);
239                         }
240                         if (polipo_pid == (Pid) 0) {
241                                 Process.spawn_async_with_pipes ("/tmp",
242                                                                 { "/usr/bin/polipo" },
243                                                                 null,
244                                                                 SpawnFlags.SEARCH_PATH,
245                                                                 null,
246                                                                 out polipo_pid);
247                         }
248
249                         /* --> proxy settings and will be set up and tor_connected will
250                          * be set to true once Tor signals 100%
251                          */
252                 } catch (SpawnError e) {
253                         Hildon.Banner.show_information (null, null, "DEBUG: Failed to spawn polipo and tor: %s".printf (e.message));
254                         return;
255                 }
256
257                 tor_log = "";
258                 if (log_label != null)
259                         log_label.label = tor_log;
260                 update_status ();
261         }
262
263         /**
264          * Stop Tor and revert proxy settings
265          */
266         private void stop_tor () {
267                 proxy_restore ();
268                 tor_connected = false;
269                 if (polipo_pid != (Pid) 0) {
270                         Process.close_pid (polipo_pid);
271                         Posix.kill ((Posix.pid_t) polipo_pid, Posix.SIGKILL);
272                         polipo_pid = (Pid) 0;
273                 }
274                 if (tor_pid != (Pid) 0) {
275                         Process.close_pid (tor_pid);
276                         Posix.kill ((Posix.pid_t) tor_pid, Posix.SIGKILL);
277                         tor_pid = (Pid) 0;
278                 }
279
280                 update_status ();
281         }
282
283         /**
284          * Setup proxy settings to route through the Tor network
285          */
286         private void proxy_setup () {
287                 if (backup == null) try {
288                         backup = new ProxyBackup ();
289                         backup.use_http_proxy = gconf.get_bool (GCONF_KEY_PROXY_HTTP_ENABLED);
290
291                         backup.http_host = gconf.get_string (GCONF_KEY_PROXY_HTTP_HOST);
292                         backup.socks_host = gconf.get_string (GCONF_KEY_PROXY_SOCKS_HOST);
293                         backup.secure_host = gconf.get_string (GCONF_KEY_PROXY_SECURE_HOST);
294                         backup.http_port = gconf.get_int (GCONF_KEY_PROXY_HTTP_PORT);
295                         backup.socks_port = gconf.get_int (GCONF_KEY_PROXY_SOCKS_PORT);
296                         backup.secure_port = gconf.get_int (GCONF_KEY_PROXY_SECURE_PORT);
297
298                         backup.mode = gconf.get_string (GCONF_KEY_PROXY_MODE);
299                 } catch (Error e) {
300                         error ("Error saving proxy settings: %s", e.message);
301                         backup = new ProxyBackup ();
302                         backup.use_http_proxy = false;
303
304                         backup.http_host = "";
305                         backup.socks_host = "";
306                         backup.secure_host = "";
307                         backup.http_port = 8080;
308                         backup.socks_port = 0;
309                         backup.secure_port = 0;
310
311                         backup.mode = "none";
312                 }
313                 try {
314                 //      Hildon.Banner.show_information (null, null, "DEBUG: Proxy setup");
315                         gconf.set_bool (GCONF_KEY_PROXY_HTTP_ENABLED, true);
316
317                         gconf.set_string (GCONF_KEY_PROXY_HTTP_HOST, "127.0.0.1");
318                         gconf.set_string (GCONF_KEY_PROXY_SOCKS_HOST, "127.0.0.1");
319                         gconf.set_string (GCONF_KEY_PROXY_SECURE_HOST, "127.0.0.1");
320                         gconf.set_int (GCONF_KEY_PROXY_HTTP_PORT, 8118);
321                         gconf.set_int (GCONF_KEY_PROXY_SOCKS_PORT, 9050);
322                         gconf.set_int (GCONF_KEY_PROXY_SECURE_PORT, 8118);
323
324                         gconf.set_string (GCONF_KEY_PROXY_MODE, "manual");
325                 } catch (Error e) {
326                         error ("Error changing proxy settings: %s", e.message);
327                 }
328         }
329
330         /**
331          * Revert proxy settings
332          */
333         private void proxy_restore () {
334                 if (backup != null) try {
335                 //      Hildon.Banner.show_information (null, null, "DEBUG: Restoring proxy settings");
336                         gconf.set_bool (GCONF_KEY_PROXY_HTTP_ENABLED, backup.use_http_proxy);
337
338                         gconf.set_string (GCONF_KEY_PROXY_HTTP_HOST, backup.http_host);
339                         gconf.set_string (GCONF_KEY_PROXY_SOCKS_HOST, backup.socks_host);
340                         gconf.set_string (GCONF_KEY_PROXY_SECURE_HOST, backup.secure_host);
341                         gconf.set_int (GCONF_KEY_PROXY_HTTP_PORT, backup.http_port);
342                         gconf.set_int (GCONF_KEY_PROXY_SOCKS_PORT, backup.socks_port);
343                         gconf.set_int (GCONF_KEY_PROXY_SECURE_PORT, backup.secure_port);
344
345                         gconf.set_string (GCONF_KEY_PROXY_MODE, backup.mode);
346                         backup = null;
347                 } catch (Error e) {
348                         error ("Error restoring proxy: %s", e.message);
349                 }
350         }
351
352         /**
353          * Show the bridge relay configuration dialog
354          */
355         private void bridges_clicked_cb () {
356                 var dialog = new BridgeDialog ();
357                 dialog.show ();
358         }
359
360         /**
361          * Check whether the IP address consists of four numbers in the 0..255 range
362          */
363         bool is_valid_ip_address (string address) {
364                 string[] ip = address.split (".");
365
366                 if (ip.length != 4)
367                         return false;
368
369                 for (int i = 0; i < ip.length; i++) {
370                         int n = ip[i].to_int ();
371                         if (n < 0 || n > 255)
372                                 return false;
373                 }
374
375                 return true;
376         }
377
378         /**
379          * Show the Tor log dialog
380          */
381         private void show_tor_log () {
382                 var dialog = new Gtk.Dialog ();
383                 var content = (Gtk.VBox) dialog.get_content_area ();
384                 content.set_size_request (-1, 5*70);
385
386                 dialog.set_title (_("Log"));
387
388                 var pannable = new Hildon.PannableArea ();
389                 pannable.mov_mode = Hildon.MovementMode.BOTH;
390                 log_label = new Gtk.Label (tor_log);
391                 log_label.set_alignment (0, 0);
392                 pannable.add_with_viewport (log_label);
393                 content.pack_start (pannable, true, true, 0);
394
395                 dialog.response.connect (() => {
396                         log_label = null;
397                 });
398
399                 dialog.show_all ();
400         }
401
402         /**
403          * Callback for the status menu button clicked signal
404          */
405         private const int RESPONSE_LOG = 1;
406         private void button_clicked_cb () {
407                 var dialog = new Gtk.Dialog ();
408                 var content = (Gtk.VBox) dialog.get_content_area ();
409                 content.set_size_request (-1, 2*70);
410
411                 dialog.set_title (_("Tor: anonymity online"));
412
413                 var check = new Hildon.CheckButton (Hildon.SizeType.FINGER_HEIGHT);
414                 check.set_label (_("Enable onion routing"));
415                 check.set_active (tor_enabled);
416                 content.pack_start (check, true, true, 0);
417
418                 var button = new Hildon.Button.with_text (Hildon.SizeType.FINGER_HEIGHT,
419                                                           Hildon.ButtonArrangement.VERTICAL,
420                                                           _("Bridge relays"),
421                                                           get_bridge_list ());
422                 button.set_style (Hildon.ButtonStyle.PICKER);
423                 button.set_alignment (0, 0.5f, 0, 0.5f);
424                 button.clicked.connect (bridges_clicked_cb);
425                 content.pack_start (button, true, true, 0);
426
427                 dialog.add_button (_("Log"), RESPONSE_LOG);
428
429                 dialog.add_button (_("Save"), Gtk.ResponseType.ACCEPT);
430                 dialog.response.connect ((response_id) => {
431                         if (response_id == RESPONSE_LOG) {
432                                 show_tor_log ();
433                                 return;
434                         }
435                         if (response_id == Gtk.ResponseType.ACCEPT) {
436                                 if (!tor_enabled && check.get_active ()) {
437                                         tor_enabled = true;
438
439                                         if (conic_connected) {
440                                                 start_tor ();
441                                         } else {
442                                                 conic.connect (ConIc.ConnectFlags.NONE);
443                                         }
444                                 } else if (tor_enabled && !check.get_active ()) {
445                                         tor_enabled = false;
446
447                                         stop_tor ();
448                                         if (conic_connected)
449                                                 conic.disconnect ();
450                                 }
451                         }
452                         dialog.destroy ();
453                 });
454
455                 dialog.show_all ();
456         }
457
458         private string get_bridge_list () {
459                 string list = null;
460                 var bridges = new SList<string> ();
461                 try {
462                         bridges = gconf.get_list (GCONF_KEY_BRIDGES, GConf.ValueType.STRING);
463                 } catch (Error e) {
464                         error ("Error loading bridges: %s", e.message);
465                 }
466                 foreach (string bridge in bridges) {
467                         if (list == null)
468                                 list = bridge;
469                         else
470                                 list += ", " + bridge;
471                 }
472                 if (list == null)
473                         list = _("None");
474
475                 return list;
476         }
477
478         /**
479          * Callback for the ConIc connection-event signal
480          */
481         private void conic_connection_event_cb (ConIc.Connection conic, ConIc.ConnectionEvent event) {
482                 var status = event.get_status ();
483                 switch (status) {
484                 case ConIc.ConnectionStatus.CONNECTED:
485                         conic_connected = true;
486                         if (tor_enabled) {
487                                 start_tor ();
488                         } else {
489                                 update_status ();
490                         }
491                         break;
492                 case ConIc.ConnectionStatus.DISCONNECTING:
493                         conic_connected = false;
494                         stop_tor ();
495                         break;
496                 case ConIc.ConnectionStatus.DISCONNECTED:
497                 case ConIc.ConnectionStatus.NETWORK_UP:
498                         // ignore
499                         break;
500                 }
501
502                 var error = event.get_error ();
503                 switch (error) {
504                 case ConIc.ConnectionError.CONNECTION_FAILED:
505                         Hildon.Banner.show_information (null, null, "DEBUG: ConIc connection failed");
506                         break;
507                 case ConIc.ConnectionError.USER_CANCELED:
508                         Hildon.Banner.show_information (null, null, "DEBUG: ConIc user canceled");
509                         break;
510                 case ConIc.ConnectionError.NONE:
511                 case ConIc.ConnectionError.INVALID_IAP:
512                         // ignore
513                         break;
514                 }
515         }
516
517         private void create_widgets () {
518                 // Status menu button
519                 button = new Hildon.Button.with_text (Hildon.SizeType.FINGER_HEIGHT,
520                                                       Hildon.ButtonArrangement.VERTICAL,
521                                                       _("The Onion Router"),
522                                                       tor_enabled ? _("Enabled") : _("Disabled"));
523                 button.set_alignment (0.0f, 0.5f, 1.0f, 1.0f);
524                 button.set_style (Hildon.ButtonStyle.PICKER);
525                 button.clicked.connect (button_clicked_cb);
526
527                 add (button);
528
529                 log_label = null;
530
531                 // Status area icon
532                 update_status ();
533
534                 show_all ();
535         }
536
537         construct {
538                 // Gettext hook-up
539                 Intl.setlocale (LocaleCategory.ALL, "");
540                 Intl.bindtextdomain (Config.GETTEXT_PACKAGE, Config.LOCALEDIR);
541                 Intl.textdomain (Config.GETTEXT_PACKAGE);
542
543                 // GConf hook-up
544                 gconf = GConf.Client.get_default ();
545                 try {
546                         tor_enabled = gconf.get_bool (GCONF_KEY_TOR_ENABLED);
547                 } catch (Error e) {
548                         error ("Failed to get GConf setting: %s", e.message);
549                 }
550                 tor_connected = false;
551
552                 // ConIc hook-up
553                 conic = new ConIc.Connection ();
554                 if (conic == null) {
555                         Hildon.Banner.show_information (null, null, "DEBUG: ConIc hook-up failed");
556                 }
557                 conic_connected = false;
558                 conic.automatic_connection_events = true;
559                 if (tor_enabled)
560                         conic.connect (ConIc.ConnectFlags.AUTOMATICALLY_TRIGGERED);
561                 conic.connection_event.connect (conic_connection_event_cb);
562
563                 // Osso hook-up
564                 osso = new Osso.Context (STATUSMENU_TOR_LIBOSSO_SERVICE_NAME,
565                                          Config.VERSION,
566                                          true,
567                                          null);
568
569                 create_widgets ();
570         }
571 }
572
573 /**
574  * Vala code can't use the HD_DEFINE_PLUGIN_MODULE macro, but it handles
575  * most of the class registration issues itself. Only this code from
576  * HD_PLUGIN_MODULE_SYMBOLS_CODE has to be has to be included manually
577  * to register with hildon-desktop:
578  */
579 [ModuleInit]
580 public void hd_plugin_module_load (TypeModule plugin) {
581         // [ModuleInit] registers types automatically
582         ((HD.PluginModule) plugin).add_type (typeof (TorStatusMenuItem));
583 }
584
585 public void hd_plugin_module_unload (HD.PluginModule plugin) {
586 }