dino/libdino/src/service/connection_manager.vala

392 lines
14 KiB
Vala
Raw Normal View History

2017-03-02 14:37:32 +00:00
using Gee;
using Xmpp;
using Dino.Entities;
namespace Dino {
public class ConnectionManager : Object {
2017-03-02 14:37:32 +00:00
public signal void stream_opened(Account account, XmppStream stream);
public signal void stream_attached_modules(Account account, XmppStream stream);
2017-03-02 14:37:32 +00:00
public signal void connection_state_changed(Account account, ConnectionState state);
public signal void connection_error(Account account, ConnectionError error);
2017-03-02 14:37:32 +00:00
public enum ConnectionState {
CONNECTED,
CONNECTING,
DISCONNECTED
}
private HashMap<Account, Connection> connections = new HashMap<Account, Connection>(Account.hash_func, Account.equals_func);
private HashMap<Account, ConnectionError> connection_errors = new HashMap<Account, ConnectionError>(Account.hash_func, Account.equals_func);
private HashMap<Account, bool> connection_ongoing = new HashMap<Account, bool>(Account.hash_func, Account.equals_func);
private HashMap<Account, bool> connection_directly_retry = new HashMap<Account, bool>(Account.hash_func, Account.equals_func);
Move to GNetworkMonitor (#236) * Move to GNetworkMonitor Dino currently talks to NetworkManager over DBus to know the state of the network. That doesn't work in a Flatpak sandbox by default though, because Flatpak filters DBus communications and only allows a very small set of things to pass (which are known to be safe). Gio provides an API to know the state of the network (and be notified of changes via a signal): GNetworkMonitor. And GNetworkMonitor works both inside a Flatpak sandbox, and in traditional builds. (in Flatpak it uses what we call a "portal", which are the clean, safe way to let apps exit their sandbox) Fixes #235 * Don't check for network connectivity for now The connectivity check really is the correct thing to do: * network_available means that the computer has network routes to "somewhere". That is, it is connected to a router. * connectivity.FULL means that the computer can access "the Internet". That is, if it is behind a router, that router is connected. As a result, only checking for network_available is not correct. Unfortunately, NetworkManager tends to wait a long time before checking for connectivity. As a result, it is possible that a transient network error leaves NetworkManager thinking that network_available is true but connectivity!=FULL, and it will wait several minutes before realizing that the Internet connexion did come back. During that time, apps checking for connectivity (e.g the whole GNOME desktop) will think they don't have access to the Internet, while apps that don't (e.g Firefox) will access the Internet just fine. Users are understandably confused when that happens. Removing the check for connectivity is an acceptable trade-off in the short-term, until this situation is improved on the NetworkManager side. https://bugzilla.gnome.org/show_bug.cgi?id=792240
2018-01-09 19:39:45 +00:00
private NetworkMonitor? network_monitor;
2017-03-02 14:37:32 +00:00
private Login1Manager? login1;
private ModuleManager module_manager;
2017-04-03 13:09:30 +00:00
public string? log_options;
2017-03-02 14:37:32 +00:00
public class ConnectionError {
public enum Source {
CONNECTION,
SASL,
2018-01-04 20:13:44 +00:00
TLS,
STREAM_ERROR
}
2018-01-04 20:13:44 +00:00
public enum Reconnect {
NOW,
LATER,
NEVER
}
public Source source;
public string? identifier;
2018-01-04 20:13:44 +00:00
public Reconnect reconnect_recomendation { get; set; default=Reconnect.NOW; }
public ConnectionError(Source source, string? identifier) {
this.source = source;
this.identifier = identifier;
}
}
2017-03-02 14:37:32 +00:00
private class Connection {
public string uuid { get; set; }
public XmppStream? stream { get; set; }
2017-03-02 14:37:32 +00:00
public ConnectionState connection_state { get; set; default = ConnectionState.DISCONNECTED; }
public DateTime? established { get; set; }
public DateTime? last_activity { get; set; }
public Connection() {
reset();
}
public void reset() {
if (stream != null) {
stream.detach_modules();
stream.disconnect.begin();
}
stream = null;
established = last_activity = null;
uuid = Xmpp.random_uuid();
}
public void make_offline() {
Xmpp.Presence.Stanza presence = new Xmpp.Presence.Stanza();
presence.type_ = Xmpp.Presence.Stanza.TYPE_UNAVAILABLE;
if (stream != null) {
stream.get_module(Presence.Module.IDENTITY).send_presence(stream, presence);
}
}
public async void disconnect_account() {
make_offline();
if (stream != null) {
try {
yield stream.disconnect();
} catch (Error e) {
debug("Error disconnecting stream: %s", e.message);
}
}
2017-03-02 14:37:32 +00:00
}
}
public ConnectionManager(ModuleManager module_manager) {
this.module_manager = module_manager;
Move to GNetworkMonitor (#236) * Move to GNetworkMonitor Dino currently talks to NetworkManager over DBus to know the state of the network. That doesn't work in a Flatpak sandbox by default though, because Flatpak filters DBus communications and only allows a very small set of things to pass (which are known to be safe). Gio provides an API to know the state of the network (and be notified of changes via a signal): GNetworkMonitor. And GNetworkMonitor works both inside a Flatpak sandbox, and in traditional builds. (in Flatpak it uses what we call a "portal", which are the clean, safe way to let apps exit their sandbox) Fixes #235 * Don't check for network connectivity for now The connectivity check really is the correct thing to do: * network_available means that the computer has network routes to "somewhere". That is, it is connected to a router. * connectivity.FULL means that the computer can access "the Internet". That is, if it is behind a router, that router is connected. As a result, only checking for network_available is not correct. Unfortunately, NetworkManager tends to wait a long time before checking for connectivity. As a result, it is possible that a transient network error leaves NetworkManager thinking that network_available is true but connectivity!=FULL, and it will wait several minutes before realizing that the Internet connexion did come back. During that time, apps checking for connectivity (e.g the whole GNOME desktop) will think they don't have access to the Internet, while apps that don't (e.g Firefox) will access the Internet just fine. Users are understandably confused when that happens. Removing the check for connectivity is an acceptable trade-off in the short-term, until this situation is improved on the NetworkManager side. https://bugzilla.gnome.org/show_bug.cgi?id=792240
2018-01-09 19:39:45 +00:00
network_monitor = GLib.NetworkMonitor.get_default();
if (network_monitor != null) {
network_monitor.network_changed.connect(on_network_changed);
network_monitor.notify["connectivity"].connect(on_network_changed);
2017-03-02 14:37:32 +00:00
}
login1 = get_login1();
if (login1 != null) {
login1.PrepareForSleep.connect(on_prepare_for_sleep);
}
Timeout.add_seconds(60, () => {
foreach (Account account in connections.keys) {
2018-01-04 20:13:44 +00:00
if (connections[account].last_activity != null &&
connections[account].last_activity.compare(new DateTime.now_utc().add_minutes(-1)) < 0) {
check_reconnect(account);
}
}
return true;
});
2017-03-02 14:37:32 +00:00
}
public XmppStream? get_stream(Account account) {
if (get_state(account) == ConnectionState.CONNECTED) {
return connections[account].stream;
2017-03-02 14:37:32 +00:00
}
return null;
}
public ConnectionState get_state(Account account) {
if (connections.has_key(account)){
return connections[account].connection_state;
2017-03-02 14:37:32 +00:00
}
return ConnectionState.DISCONNECTED;
}
public ConnectionError? get_error(Account account) {
if (connection_errors.has_key(account)) {
return connection_errors[account];
}
return null;
}
2018-03-10 18:46:08 +00:00
public Collection<Account> get_managed_accounts() {
return connections.keys;
2017-03-02 14:37:32 +00:00
}
2019-03-15 19:56:19 +00:00
public void connect_account(Account account) {
if (!connections.has_key(account)) {
connections[account] = new Connection();
connection_ongoing[account] = false;
connection_directly_retry[account] = false;
connect_stream.begin(account);
2017-03-02 14:37:32 +00:00
} else {
check_reconnect(account);
}
}
public void make_offline_all() {
2018-09-16 11:54:47 +00:00
foreach (Account account in connections.keys) {
make_offline(account);
}
}
private void make_offline(Account account) {
connections[account].make_offline();
2017-03-02 14:37:32 +00:00
change_connection_state(account, ConnectionState.DISCONNECTED);
}
public async void disconnect_account(Account account) {
if (connections.has_key(account)) {
2018-09-16 11:54:47 +00:00
make_offline(account);
2021-04-09 21:59:03 +00:00
connections[account].disconnect_account.begin();
connections.unset(account);
2017-03-02 14:37:32 +00:00
}
}
private async void connect_stream(Account account, string? resource = null) {
if (!connections.has_key(account)) return;
debug("[%s] (Maybe) Establishing a new connection", account.bare_jid.to_string());
connection_errors.unset(account);
2017-03-02 14:37:32 +00:00
if (resource == null) resource = account.resourcepart;
XmppStreamResult stream_result;
if (connection_ongoing[account]) {
debug("[%s] Connection attempt already in progress. Directly retry if it fails.", account.bare_jid.to_string());
connection_directly_retry[account] = true;
return;
} else if (connections[account].stream != null) {
debug("[%s] Cancelling connecting because there is already a stream", account.bare_jid.to_string());
return;
} else {
connection_ongoing[account] = true;
connection_directly_retry[account] = false;
change_connection_state(account, ConnectionState.CONNECTING);
stream_result = yield Xmpp.establish_stream(account.bare_jid, module_manager.get_modules(account, resource), log_options,
(peer_cert, errors) => { return on_invalid_certificate(account.domainpart, peer_cert, errors); }
);
connections[account].stream = stream_result.stream;
connection_ongoing[account] = false;
2017-03-02 14:37:32 +00:00
}
if (stream_result.stream == null) {
if (stream_result.tls_errors != null) {
set_connection_error(account, new ConnectionError(ConnectionError.Source.TLS, null) { reconnect_recomendation=ConnectionError.Reconnect.NEVER});
return;
}
debug("[%s] Could not connect", account.bare_jid.to_string());
change_connection_state(account, ConnectionState.DISCONNECTED);
check_reconnect(account, connection_directly_retry[account]);
return;
}
XmppStream stream = stream_result.stream;
2019-03-15 19:56:19 +00:00
debug("[%s] New connection with resource %s: %p", account.bare_jid.to_string(), resource, stream);
2017-03-02 14:37:32 +00:00
connections[account].established = new DateTime.now_utc();
2017-08-12 21:14:50 +00:00
stream.attached_modules.connect((stream) => {
stream_attached_modules(account, stream);
2017-03-02 14:37:32 +00:00
change_connection_state(account, ConnectionState.CONNECTED);
// stream.get_module(Xep.Muji.Module.IDENTITY).join_call(stream, new Jid("test@muc.poez.io"), true);
2017-03-02 14:37:32 +00:00
});
2018-08-19 17:56:46 +00:00
stream.get_module(Sasl.Module.IDENTITY).received_auth_failure.connect((stream, node) => {
2018-01-04 20:13:44 +00:00
set_connection_error(account, new ConnectionError(ConnectionError.Source.SASL, null));
});
string connection_uuid = connections[account].uuid;
stream.received_node.connect(() => {
if (connections[account].uuid == connection_uuid) {
connections[account].last_activity = new DateTime.now_utc();
} else {
warning("Got node for outdated connection");
}
});
2017-03-02 14:37:32 +00:00
stream_opened(account, stream);
2017-11-11 20:29:13 +00:00
try {
yield stream.loop();
2017-11-11 20:29:13 +00:00
} catch (Error e) {
debug("[%s %p] Connection error: %s", account.bare_jid.to_string(), stream, e.message);
2017-11-11 20:29:13 +00:00
change_connection_state(account, ConnectionState.DISCONNECTED);
connections[account].reset();
2017-11-11 20:29:13 +00:00
StreamError.Flag? flag = stream.get_flag(StreamError.Flag.IDENTITY);
if (flag != null) {
2019-03-15 19:56:19 +00:00
warning(@"[%s %p] Stream Error: %s", account.bare_jid.to_string(), stream, flag.error_type);
set_connection_error(account, new ConnectionError(ConnectionError.Source.STREAM_ERROR, flag.error_type));
2017-11-11 20:29:13 +00:00
2019-03-15 19:56:19 +00:00
if (flag.resource_rejected) {
connect_stream.begin(account, account.resourcepart + "-" + random_uuid());
return;
2018-03-10 18:46:08 +00:00
}
2019-03-15 19:56:19 +00:00
}
ConnectionError? error = connection_errors[account];
if (error != null && error.source == ConnectionError.Source.SASL) {
2018-03-10 18:46:08 +00:00
return;
}
2019-03-15 19:56:19 +00:00
check_reconnect(account);
2017-03-02 14:37:32 +00:00
}
}
private void check_reconnects() {
foreach (Account account in connections.keys) {
2017-03-02 14:37:32 +00:00
check_reconnect(account);
}
}
private void check_reconnect(Account account, bool directly_reconnect = false) {
if (!connections.has_key(account)) return;
bool acked = false;
DateTime? last_activity_was = connections[account].last_activity;
if (connections[account].stream == null) {
Timeout.add_seconds(10, () => {
if (!connections.has_key(account)) return false;
if (connections[account].stream != null) return false;
if (connections[account].last_activity != last_activity_was) return false;
connect_stream.begin(account);
return false;
});
return;
}
XmppStream stream = connections[account].stream;
2020-04-24 12:19:42 +00:00
stream.get_module(Xep.Ping.Module.IDENTITY).send_ping.begin(stream, account.bare_jid.domain_jid, () => {
acked = true;
2019-03-15 19:56:19 +00:00
if (connections[account].stream != stream) return;
change_connection_state(account, ConnectionState.CONNECTED);
});
2017-03-02 14:37:32 +00:00
Timeout.add_seconds(10, () => {
if (!connections.has_key(account)) return false;
if (connections[account].stream != stream) return false;
if (acked) return false;
if (connections[account].last_activity != last_activity_was) return false;
2017-03-02 14:37:32 +00:00
// Reconnect. Nothing gets through the stream.
2019-03-15 19:56:19 +00:00
debug("[%s %p] Ping timeouted. Reconnecting", account.bare_jid.to_string(), stream);
2017-03-02 14:37:32 +00:00
change_connection_state(account, ConnectionState.DISCONNECTED);
connections[account].reset();
connect_stream.begin(account);
2017-03-02 14:37:32 +00:00
return false;
});
}
Move to GNetworkMonitor (#236) * Move to GNetworkMonitor Dino currently talks to NetworkManager over DBus to know the state of the network. That doesn't work in a Flatpak sandbox by default though, because Flatpak filters DBus communications and only allows a very small set of things to pass (which are known to be safe). Gio provides an API to know the state of the network (and be notified of changes via a signal): GNetworkMonitor. And GNetworkMonitor works both inside a Flatpak sandbox, and in traditional builds. (in Flatpak it uses what we call a "portal", which are the clean, safe way to let apps exit their sandbox) Fixes #235 * Don't check for network connectivity for now The connectivity check really is the correct thing to do: * network_available means that the computer has network routes to "somewhere". That is, it is connected to a router. * connectivity.FULL means that the computer can access "the Internet". That is, if it is behind a router, that router is connected. As a result, only checking for network_available is not correct. Unfortunately, NetworkManager tends to wait a long time before checking for connectivity. As a result, it is possible that a transient network error leaves NetworkManager thinking that network_available is true but connectivity!=FULL, and it will wait several minutes before realizing that the Internet connexion did come back. During that time, apps checking for connectivity (e.g the whole GNOME desktop) will think they don't have access to the Internet, while apps that don't (e.g Firefox) will access the Internet just fine. Users are understandably confused when that happens. Removing the check for connectivity is an acceptable trade-off in the short-term, until this situation is improved on the NetworkManager side. https://bugzilla.gnome.org/show_bug.cgi?id=792240
2018-01-09 19:39:45 +00:00
private bool network_is_online() {
/* FIXME: We should also check for connectivity eventually. For more
* details on why we don't do it for now, see:
*
* - https://github.com/dino/dino/pull/236#pullrequestreview-86851793
* - https://bugzilla.gnome.org/show_bug.cgi?id=792240
*/
return network_monitor != null && network_monitor.network_available;
}
private void on_network_changed() {
if (network_is_online()) {
2019-03-15 19:56:19 +00:00
debug("NetworkMonitor: Network reported online");
2017-03-02 14:37:32 +00:00
check_reconnects();
} else {
2019-03-15 19:56:19 +00:00
debug("NetworkMonitor: Network reported offline");
foreach (Account account in connections.keys) {
2017-03-02 14:37:32 +00:00
change_connection_state(account, ConnectionState.DISCONNECTED);
}
}
}
private async void on_prepare_for_sleep(bool suspend) {
foreach (Account account in connections.keys) {
2017-03-02 14:37:32 +00:00
change_connection_state(account, ConnectionState.DISCONNECTED);
}
if (suspend) {
2019-03-15 19:56:19 +00:00
debug("Login1: Device suspended");
foreach (Account account in connections.keys) {
2017-03-02 14:37:32 +00:00
try {
2018-03-10 18:46:08 +00:00
make_offline(account);
2021-04-25 17:49:10 +00:00
if (connections[account].stream != null) {
yield connections[account].stream.disconnect();
}
2018-09-16 11:54:47 +00:00
} catch (Error e) {
2019-03-15 19:56:19 +00:00
debug("Error disconnecting stream %p: %s", connections[account].stream, e.message);
2018-09-16 11:54:47 +00:00
}
2017-03-02 14:37:32 +00:00
}
} else {
2019-03-15 19:56:19 +00:00
debug("Login1: Device un-suspend");
2017-03-02 14:37:32 +00:00
check_reconnects();
}
}
private void change_connection_state(Account account, ConnectionState state) {
if (connections.has_key(account)) {
connections[account].connection_state = state;
connection_state_changed(account, state);
}
}
2018-01-04 20:13:44 +00:00
private void set_connection_error(Account account, ConnectionError error) {
connection_errors[account] = error;
connection_error(account, error);
2017-03-02 14:37:32 +00:00
}
public static bool on_invalid_certificate(string domain, TlsCertificate peer_cert, TlsCertificateFlags errors) {
if (domain.has_suffix(".onion") && errors == TlsCertificateFlags.UNKNOWN_CA) {
// It's barely possible for .onion servers to provide a non-self-signed cert.
// But that's fine because encryption is provided independently though TOR.
warning("Accepting TLS certificate from unknown CA from .onion address %s", domain);
return true;
}
return false;
}
2017-03-02 14:37:32 +00:00
}
}