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