/* This file is part of status-area-applet-tor.
*
* Copyright (C) 2010 Philipp Zabel
*
* status-area-applet-tor is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* status-area-applet-tor is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with status-area-applet-tor. If not, see .
*/
[Compact]
class ProxyBackup {
public bool use_http_proxy;
public string http_host;
public string socks_host;
public string secure_host;
public int http_port;
public int socks_port;
public int secure_port;
public string mode;
}
class TorStatusMenuItem : HD.StatusMenuItem {
private const string STATUSMENU_TOR_LIBOSSO_SERVICE_NAME = "tor_status_menu_item";
private const int STATUS_MENU_ICON_SIZE = 48;
private const int STATUS_AREA_ICON_SIZE = 18;
private const string GCONF_DIR_TOR = "/apps/maemo/tor";
private const string GCONF_KEY_TOR_ENABLED = GCONF_DIR_TOR + "/enabled";
private const string GCONF_KEY_BRIDGES = GCONF_DIR_TOR + "/bridges";
private const string GCONF_DIR_PROXY_HTTP = "/system/http_proxy";
private const string GCONF_KEY_PROXY_HTTP_ENABLED = GCONF_DIR_PROXY_HTTP + "/use_http_proxy";
private const string GCONF_KEY_PROXY_HTTP_HOST = GCONF_DIR_PROXY_HTTP + "/host";
private const string GCONF_KEY_PROXY_HTTP_PORT = GCONF_DIR_PROXY_HTTP + "/port";
private const string GCONF_DIR_PROXY = "/system/proxy";
private const string GCONF_KEY_PROXY_MODE = GCONF_DIR_PROXY + "/mode";
private const string GCONF_KEY_PROXY_SOCKS_HOST = GCONF_DIR_PROXY + "/socks_host";
private const string GCONF_KEY_PROXY_SOCKS_PORT = GCONF_DIR_PROXY + "/socks_port";
private const string GCONF_KEY_PROXY_SECURE_HOST = GCONF_DIR_PROXY + "/secure_host";
private const string GCONF_KEY_PROXY_SECURE_PORT = GCONF_DIR_PROXY + "/secure_port";
// Widgets
Hildon.Button button;
Gtk.Label log_label;
// Icons
Gdk.Pixbuf icon_connecting;
Gdk.Pixbuf icon_connected;
Gtk.Image icon_enabled;
Gtk.Image icon_disabled;
// ConIc, GConf and Osso context
Osso.Context osso;
GConf.Client gconf;
ConIc.Connection conic;
bool conic_connected;
// Internal state
bool tor_enabled;
bool tor_connected;
Pid tor_pid;
int tor_stdout;
Pid polipo_pid;
ProxyBackup backup;
string tor_log;
TorControl.Connection tor_control;
string password;
/**
* Update status area icon and status menu button value
*/
private void update_status () {
if (tor_enabled && tor_connected && icon_connected == null) try {
var icon_theme = Gtk.IconTheme.get_default ();
var pixbuf = icon_theme.load_icon ("statusarea_tor_connected",
STATUS_AREA_ICON_SIZE,
Gtk.IconLookupFlags.NO_SVG);
icon_connected = pixbuf;
} catch (Error e) {
error (e.message);
}
if (tor_enabled && !tor_connected && icon_connecting == null) try {
var icon_theme = Gtk.IconTheme.get_default ();
var pixbuf = icon_theme.load_icon ("statusarea_tor_connecting",
STATUS_AREA_ICON_SIZE,
Gtk.IconLookupFlags.NO_SVG);
icon_connecting = pixbuf;
} catch (Error e) {
error (e.message);
}
if (tor_enabled && icon_enabled == null) try {
var icon_theme = Gtk.IconTheme.get_default();
var pixbuf = icon_theme.load_icon ("statusarea_tor_enabled",
STATUS_MENU_ICON_SIZE,
Gtk.IconLookupFlags.NO_SVG);
icon_enabled = new Gtk.Image.from_pixbuf (pixbuf);
} catch (Error e) {
error (e.message);
}
if (!tor_enabled && icon_disabled == null) try {
var icon_theme = Gtk.IconTheme.get_default();
var pixbuf = icon_theme.load_icon ("statusarea_tor_disabled",
STATUS_MENU_ICON_SIZE,
Gtk.IconLookupFlags.NO_SVG);
icon_disabled = new Gtk.Image.from_pixbuf (pixbuf);
} catch (Error e) {
error (e.message);
}
if (conic_connected && tor_enabled) {
set_status_area_icon (tor_connected ? icon_connected : icon_connecting);
button.set_value (tor_connected ? _("Connected") : _("Connecting ..."));
} else {
set_status_area_icon (null);
button.set_value (tor_enabled ? _("Disconnected") : _("Disabled"));
}
button.set_image (tor_enabled ? icon_enabled : icon_disabled);
}
/**
* Callback for Tor daemon line output
*/
private bool tor_io_func (IOChannel source, IOCondition condition) {
if ((condition & (IOCondition.IN | IOCondition.PRI)) != 0) {
string line = null;
size_t length;
try {
/* var status = */ source.read_line (out line, out length, null);
tor_log += line;
if (log_label != null)
log_label.label = tor_log;
if ("[notice]" in line) {
if ("Bootstrapped 100%" in line) {
tor_connected = true;
proxy_setup ();
update_status ();
}
if ("Opening Control listener on 127.0.0.1:9051" in line) {
tor_control = new TorControl.Connection ();
tor_control_auth.begin ();
}
} else {
// FIXME
Hildon.Banner.show_information (null, null, "DEBUG: %s".printf (line));
}
} catch (Error e) {
// FIXME
Hildon.Banner.show_information (null, null, "Error: %s".printf (e.message));
}
}
if ((condition & (IOCondition.ERR | IOCondition.HUP | IOCondition.NVAL)) != 0) {
return false;
}
return true;
}
/**
* Authenticate with Tor on the control channel
*/
private async void tor_control_auth () throws Error {
yield tor_control.authenticate_async (password);
try {
var bridges = gconf.get_list (GCONF_KEY_BRIDGES, GConf.ValueType.STRING);
if (bridges.length () > 0) {
// 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");
}
}
} catch (Error e) {
error ("Error loading bridges: %s", e.message);
}
}
/**
* Start Tor and setup proxy settings
*/
private void start_tor () {
try {
if (tor_pid == (Pid) 0) {
string[] tor_hash_argv = {
"/usr/sbin/tor",
"--hash-password", "",
null
};
var tv = TimeVal ();
Random.set_seed ((uint32) tv.tv_usec);
password = "tor-status-%8x".printf (Random.next_int ());
tor_hash_argv[2] = password;
string hash;
Process.spawn_sync ("/tmp", tor_hash_argv, null, 0, null, out hash);
hash = hash.str ("16:").replace ("\n", "");
if (hash == null) {
Hildon.Banner.show_information (null, null,
"Failed to get hash");
return;
}
string[] tor_argv = {
"/usr/sbin/tor",
"--ControlPort", "9051",
"--HashedControlPassword", "",
null
};
tor_argv[4] = hash;
Process.spawn_async_with_pipes ("/tmp",
tor_argv,
null,
SpawnFlags.SEARCH_PATH,
null,
out tor_pid,
null,
out tor_stdout);
var channel = new IOChannel.unix_new (tor_stdout);
channel.add_watch (IOCondition.IN | IOCondition.PRI | IOCondition.ERR | IOCondition.HUP | IOCondition.NVAL, tor_io_func);
}
if (polipo_pid == (Pid) 0) {
Process.spawn_async_with_pipes ("/tmp",
{ "/usr/bin/polipo" },
null,
SpawnFlags.SEARCH_PATH,
null,
out polipo_pid);
}
/* --> proxy settings and will be set up and tor_connected will
* be set to true once Tor signals 100%
*/
} catch (SpawnError e) {
Hildon.Banner.show_information (null, null, "DEBUG: Failed to spawn polipo and tor: %s".printf (e.message));
return;
}
tor_log = "";
if (log_label != null)
log_label.label = tor_log;
update_status ();
}
/**
* Stop Tor and revert proxy settings
*/
private void stop_tor () {
proxy_restore ();
tor_connected = false;
if (polipo_pid != (Pid) 0) {
Process.close_pid (polipo_pid);
Posix.kill ((Posix.pid_t) polipo_pid, Posix.SIGKILL);
polipo_pid = (Pid) 0;
}
if (tor_pid != (Pid) 0) {
Process.close_pid (tor_pid);
Posix.kill ((Posix.pid_t) tor_pid, Posix.SIGKILL);
tor_pid = (Pid) 0;
}
update_status ();
}
/**
* Setup proxy settings to route through the Tor network
*/
private void proxy_setup () {
if (backup == null) try {
backup = new ProxyBackup ();
backup.use_http_proxy = gconf.get_bool (GCONF_KEY_PROXY_HTTP_ENABLED);
backup.http_host = gconf.get_string (GCONF_KEY_PROXY_HTTP_HOST);
backup.socks_host = gconf.get_string (GCONF_KEY_PROXY_SOCKS_HOST);
backup.secure_host = gconf.get_string (GCONF_KEY_PROXY_SECURE_HOST);
backup.http_port = gconf.get_int (GCONF_KEY_PROXY_HTTP_PORT);
backup.socks_port = gconf.get_int (GCONF_KEY_PROXY_SOCKS_PORT);
backup.secure_port = gconf.get_int (GCONF_KEY_PROXY_SECURE_PORT);
backup.mode = gconf.get_string (GCONF_KEY_PROXY_MODE);
} catch (Error e) {
error ("Error saving proxy settings: %s", e.message);
backup = new ProxyBackup ();
backup.use_http_proxy = false;
backup.http_host = "";
backup.socks_host = "";
backup.secure_host = "";
backup.http_port = 8080;
backup.socks_port = 0;
backup.secure_port = 0;
backup.mode = "none";
}
try {
// Hildon.Banner.show_information (null, null, "DEBUG: Proxy setup");
gconf.set_bool (GCONF_KEY_PROXY_HTTP_ENABLED, true);
gconf.set_string (GCONF_KEY_PROXY_HTTP_HOST, "127.0.0.1");
gconf.set_string (GCONF_KEY_PROXY_SOCKS_HOST, "127.0.0.1");
gconf.set_string (GCONF_KEY_PROXY_SECURE_HOST, "127.0.0.1");
gconf.set_int (GCONF_KEY_PROXY_HTTP_PORT, 8118);
gconf.set_int (GCONF_KEY_PROXY_SOCKS_PORT, 9050);
gconf.set_int (GCONF_KEY_PROXY_SECURE_PORT, 8118);
gconf.set_string (GCONF_KEY_PROXY_MODE, "manual");
} catch (Error e) {
error ("Error changing proxy settings: %s", e.message);
}
}
/**
* Revert proxy settings
*/
private void proxy_restore () {
if (backup != null) try {
// Hildon.Banner.show_information (null, null, "DEBUG: Restoring proxy settings");
gconf.set_bool (GCONF_KEY_PROXY_HTTP_ENABLED, backup.use_http_proxy);
gconf.set_string (GCONF_KEY_PROXY_HTTP_HOST, backup.http_host);
gconf.set_string (GCONF_KEY_PROXY_SOCKS_HOST, backup.socks_host);
gconf.set_string (GCONF_KEY_PROXY_SECURE_HOST, backup.secure_host);
gconf.set_int (GCONF_KEY_PROXY_HTTP_PORT, backup.http_port);
gconf.set_int (GCONF_KEY_PROXY_SOCKS_PORT, backup.socks_port);
gconf.set_int (GCONF_KEY_PROXY_SECURE_PORT, backup.secure_port);
gconf.set_string (GCONF_KEY_PROXY_MODE, backup.mode);
backup = null;
} catch (Error e) {
error ("Error restoring proxy: %s", e.message);
}
}
/**
* Show the bridge relay configuration dialog
*/
private void bridges_clicked_cb () {
var dialog = new BridgeDialog ();
dialog.show ();
}
/**
* 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;
log_label = new Gtk.Label (tor_log);
log_label.set_alignment (0, 0);
pannable.add_with_viewport (log_label);
content.pack_start (pannable, true, true, 0);
dialog.response.connect (() => {
log_label = null;
});
dialog.show_all ();
}
/**
* Callback for the status menu button clicked signal
*/
private const int RESPONSE_LOG = 1;
private void button_clicked_cb () {
var dialog = new Gtk.Dialog ();
var content = (Gtk.VBox) dialog.get_content_area ();
content.set_size_request (-1, 2*70);
dialog.set_title (_("Tor: anonymity online"));
var check = new Hildon.CheckButton (Hildon.SizeType.FINGER_HEIGHT);
check.set_label (_("Enable onion routing"));
check.set_active (tor_enabled);
content.pack_start (check, true, true, 0);
var button = new Hildon.Button.with_text (Hildon.SizeType.FINGER_HEIGHT,
Hildon.ButtonArrangement.VERTICAL,
_("Bridge relays"),
get_bridge_list ());
button.set_style (Hildon.ButtonStyle.PICKER);
button.set_alignment (0, 0.5f, 0, 0.5f);
button.clicked.connect (bridges_clicked_cb);
content.pack_start (button, true, true, 0);
dialog.add_button (_("Log"), RESPONSE_LOG);
dialog.add_button (_("Save"), Gtk.ResponseType.ACCEPT);
dialog.response.connect ((response_id) => {
if (response_id == RESPONSE_LOG) {
show_tor_log ();
return;
}
if (response_id == Gtk.ResponseType.ACCEPT) {
if (!tor_enabled && check.get_active ()) {
tor_enabled = true;
if (conic_connected) {
start_tor ();
} else {
conic.connect (ConIc.ConnectFlags.NONE);
}
} else if (tor_enabled && !check.get_active ()) {
tor_enabled = false;
stop_tor ();
if (conic_connected)
conic.disconnect ();
}
}
dialog.destroy ();
});
dialog.show_all ();
}
private string get_bridge_list () {
string list = null;
var bridges = new SList ();
try {
bridges = gconf.get_list (GCONF_KEY_BRIDGES, GConf.ValueType.STRING);
} catch (Error e) {
error ("Error loading bridges: %s", e.message);
}
foreach (string bridge in bridges) {
if (list == null)
list = bridge;
else
list += ", " + bridge;
}
if (list == null)
list = _("None");
return list;
}
/**
* Callback for the ConIc connection-event signal
*/
private void conic_connection_event_cb (ConIc.Connection conic, ConIc.ConnectionEvent event) {
var status = event.get_status ();
switch (status) {
case ConIc.ConnectionStatus.CONNECTED:
conic_connected = true;
if (tor_enabled) {
start_tor ();
} else {
update_status ();
}
break;
case ConIc.ConnectionStatus.DISCONNECTING:
conic_connected = false;
stop_tor ();
break;
case ConIc.ConnectionStatus.DISCONNECTED:
case ConIc.ConnectionStatus.NETWORK_UP:
// ignore
break;
}
var error = event.get_error ();
switch (error) {
case ConIc.ConnectionError.CONNECTION_FAILED:
Hildon.Banner.show_information (null, null, "DEBUG: ConIc connection failed");
break;
case ConIc.ConnectionError.USER_CANCELED:
Hildon.Banner.show_information (null, null, "DEBUG: ConIc user canceled");
break;
case ConIc.ConnectionError.NONE:
case ConIc.ConnectionError.INVALID_IAP:
// ignore
break;
}
}
private void create_widgets () {
// Status menu button
button = new Hildon.Button.with_text (Hildon.SizeType.FINGER_HEIGHT,
Hildon.ButtonArrangement.VERTICAL,
_("The Onion Router"),
tor_enabled ? _("Enabled") : _("Disabled"));
button.set_alignment (0.0f, 0.5f, 1.0f, 1.0f);
button.set_style (Hildon.ButtonStyle.PICKER);
button.clicked.connect (button_clicked_cb);
add (button);
log_label = null;
// Status area icon
update_status ();
show_all ();
}
construct {
// Gettext hook-up
Intl.setlocale (LocaleCategory.ALL, "");
Intl.bindtextdomain (Config.GETTEXT_PACKAGE, Config.LOCALEDIR);
Intl.textdomain (Config.GETTEXT_PACKAGE);
// GConf hook-up
gconf = GConf.Client.get_default ();
try {
tor_enabled = gconf.get_bool (GCONF_KEY_TOR_ENABLED);
} catch (Error e) {
error ("Failed to get GConf setting: %s", e.message);
}
tor_connected = false;
// ConIc hook-up
conic = new ConIc.Connection ();
if (conic == null) {
Hildon.Banner.show_information (null, null, "DEBUG: ConIc hook-up failed");
}
conic_connected = false;
conic.automatic_connection_events = true;
if (tor_enabled)
conic.connect (ConIc.ConnectFlags.AUTOMATICALLY_TRIGGERED);
conic.connection_event.connect (conic_connection_event_cb);
// Osso hook-up
osso = new Osso.Context (STATUSMENU_TOR_LIBOSSO_SERVICE_NAME,
Config.VERSION,
true,
null);
create_widgets ();
}
}
/**
* Vala code can't use the HD_DEFINE_PLUGIN_MODULE macro, but it handles
* most of the class registration issues itself. Only this code from
* HD_PLUGIN_MODULE_SYMBOLS_CODE has to be has to be included manually
* to register with hildon-desktop:
*/
[ModuleInit]
public void hd_plugin_module_load (TypeModule plugin) {
// [ModuleInit] registers types automatically
((HD.PluginModule) plugin).add_type (typeof (TorStatusMenuItem));
}
public void hd_plugin_module_unload (HD.PluginModule plugin) {
}