dino/xmpp-vala/src/core/xmpp_stream.vala

400 lines
14 KiB
Vala
Raw Normal View History

2017-03-02 14:37:32 +00:00
using Gee;
namespace Xmpp {
2017-03-02 14:37:32 +00:00
public errordomain IOStreamError {
READ,
WRITE,
CONNECT,
2018-01-04 20:13:44 +00:00
DISCONNECT,
TLS
2017-03-02 14:37:32 +00:00
}
public class XmppStream {
public const string NS_URI = "http://etherx.jabber.org/streams";
2017-03-02 14:37:32 +00:00
public Jid remote_name;
2017-04-03 13:09:30 +00:00
public XmppLog log = new XmppLog();
2017-03-02 14:37:32 +00:00
public StanzaNode? features { get; private set; default = new StanzaNode.build("features", NS_URI); }
private IOStream? stream;
private StanzaReader? reader;
private StanzaWriter? writer;
2017-08-12 21:14:50 +00:00
public Gee.List<XmppStreamFlag> flags { get; private set; default=new ArrayList<XmppStreamFlag>(); }
public Gee.List<XmppStreamModule> modules { get; private set; default=new ArrayList<XmppStreamModule>(); }
2017-08-09 18:44:15 +00:00
private Gee.List<ConnectionProvider> connection_providers = new ArrayList<ConnectionProvider>();
2017-08-12 21:14:50 +00:00
public bool negotiation_complete { get; set; default=false; }
2017-03-02 14:37:32 +00:00
private bool setup_needed = false;
2017-08-12 21:14:50 +00:00
private bool non_negotiation_modules_attached = false;
private bool disconnected = false;
2017-03-02 14:37:32 +00:00
public signal void received_node(XmppStream stream, StanzaNode node);
public signal void received_root_node(XmppStream stream, StanzaNode node);
public signal void received_features_node(XmppStream stream);
public signal void received_message_stanza(XmppStream stream, StanzaNode node);
public signal void received_presence_stanza(XmppStream stream, StanzaNode node);
public signal void received_iq_stanza(XmppStream stream, StanzaNode node);
public signal void received_nonza(XmppStream stream, StanzaNode node);
public signal void stream_negotiated(XmppStream stream);
2017-08-12 21:14:50 +00:00
public signal void attached_modules(XmppStream stream);
2017-03-02 14:37:32 +00:00
2017-08-09 18:44:15 +00:00
public XmppStream() {
register_connection_provider(new StartTlsConnectionProvider());
}
2017-11-11 20:29:13 +00:00
public async void connect(string? remote_name = null) throws IOStreamError {
if (remote_name != null) this.remote_name = Jid.parse(remote_name);
2017-08-12 21:14:50 +00:00
attach_negotation_modules();
2017-03-02 14:37:32 +00:00
try {
2017-08-09 18:44:15 +00:00
int min_priority = -1;
ConnectionProvider? best_provider = null;
foreach (ConnectionProvider connection_provider in connection_providers) {
int? priority = yield connection_provider.get_priority(this.remote_name);
2017-08-09 18:44:15 +00:00
if (priority != null && (priority < min_priority || min_priority == -1)) {
min_priority = priority;
best_provider = connection_provider;
}
}
IOStream? stream = null;
if (best_provider != null) {
stream = yield best_provider.connect(this);
2018-01-04 20:13:44 +00:00
}
if (stream == null) {
debug("Connecting to %s, xmpp-client, tcp (fallback)", this.remote_name.to_string());
stream = yield (new SocketClient()).connect_to_host_async(this.remote_name.to_string(), 5222);
}
2018-01-04 20:13:44 +00:00
if (stream == null) {
throw new IOStreamError.CONNECT("client.connect() returned null");
}
reset_stream((!)stream);
2017-03-02 14:37:32 +00:00
} catch (Error e) {
debug("[%p] Could not connect to server: %s", this, e.message);
2017-03-02 14:37:32 +00:00
throw new IOStreamError.CONNECT(e.message);
}
debug("Connected to %s", remote_name);
2017-11-11 20:29:13 +00:00
yield loop();
2017-03-02 14:37:32 +00:00
}
public async void disconnect() throws IOStreamError, XmlError, IOError {
disconnected = true;
if (writer == null || reader == null || stream == null) {
throw new IOStreamError.DISCONNECT("trying to disconnect, but no stream open");
}
2017-04-03 13:09:30 +00:00
log.str("OUT", "</stream:stream>");
yield writer.write("</stream:stream>");
reader.cancel();
yield stream.close_async();
2017-03-02 14:37:32 +00:00
}
public void reset_stream(IOStream stream) {
this.stream = stream;
reader = new StanzaReader.for_stream(stream.input_stream);
writer = new StanzaWriter.for_stream(stream.output_stream);
require_setup();
}
public void require_setup() {
setup_needed = true;
}
public bool is_setup_needed() {
return setup_needed;
}
2017-11-11 20:29:13 +00:00
public async StanzaNode read() throws IOStreamError {
StanzaReader? reader = this.reader;
2017-03-02 14:37:32 +00:00
if (reader == null) throw new IOStreamError.READ("trying to read, but no stream open");
try {
2017-11-11 20:29:13 +00:00
StanzaNode node = yield ((!)reader).read_node();
2017-04-03 13:09:30 +00:00
log.node("IN", node);
2017-03-02 14:37:32 +00:00
return node;
} catch (XmlError e) {
throw new IOStreamError.READ(e.message);
}
}
public void write(StanzaNode node) {
write_async.begin(node);
}
public async void write_async(StanzaNode node) throws IOStreamError {
StanzaWriter? writer = this.writer;
2017-03-02 14:37:32 +00:00
if (writer == null) throw new IOStreamError.WRITE("trying to write, but no stream open");
try {
2017-04-03 13:09:30 +00:00
log.node("OUT", node);
yield ((!)writer).write_node(node);
2017-03-02 14:37:32 +00:00
} catch (XmlError e) {
throw new IOStreamError.WRITE(e.message);
}
}
internal IOStream? get_stream() {
2017-03-02 14:37:32 +00:00
return stream;
}
public void add_flag(XmppStreamFlag flag) {
flags.add(flag);
}
public bool has_flag<T>(FlagIdentity<T>? identity) {
return get_flag(identity) != null;
}
public T? get_flag<T>(FlagIdentity<T>? identity) {
if (identity == null) return null;
2017-03-02 14:37:32 +00:00
foreach (var flag in flags) {
if (((!)identity).matches(flag)) return ((!)identity).cast(flag);
2017-03-02 14:37:32 +00:00
}
return null;
}
public void remove_flag(XmppStreamFlag flag) {
flags.remove(flag);
}
public XmppStream add_module(XmppStreamModule module) {
2018-03-10 18:46:08 +00:00
foreach (XmppStreamModule m in modules) {
if (m.get_ns() == module.get_ns() && m.get_id() == module.get_id()) {
2019-03-15 19:56:19 +00:00
warning("[%p] Adding already added module: %s\n", this, module.get_id());
2018-03-10 18:46:08 +00:00
return this;
}
}
2017-03-02 14:37:32 +00:00
modules.add(module);
2017-08-12 21:14:50 +00:00
if (negotiation_complete) module.attach(this);
2017-03-02 14:37:32 +00:00
return this;
}
2017-09-26 15:01:06 +00:00
public void detach_modules() {
2018-01-04 20:13:44 +00:00
foreach (XmppStreamModule module in modules) {
if (!(module is XmppStreamNegotiationModule) && !negotiation_complete) continue;
module.detach(this);
}
2017-03-02 14:37:32 +00:00
}
2017-03-10 20:13:35 +00:00
public T? get_module<T>(ModuleIdentity<T>? identity) {
if (identity == null) return null;
2017-03-02 14:37:32 +00:00
foreach (var module in modules) {
if (((!)identity).matches(module)) return ((!)identity).cast(module);
2017-03-02 14:37:32 +00:00
}
return null;
}
2017-08-09 18:44:15 +00:00
public void register_connection_provider(ConnectionProvider connection_provider) {
connection_providers.add(connection_provider);
}
2017-08-12 21:14:50 +00:00
public bool is_negotiation_active() {
foreach (XmppStreamModule module in modules) {
if (module is XmppStreamNegotiationModule) {
XmppStreamNegotiationModule negotiation_module = (XmppStreamNegotiationModule) module;
if (negotiation_module.negotiation_active(this)) return true;
}
}
return false;
}
2017-11-11 20:29:13 +00:00
private async void setup() throws IOStreamError {
2017-04-03 13:09:30 +00:00
StanzaNode outs = new StanzaNode.build("stream", "http://etherx.jabber.org/streams")
.put_attribute("to", remote_name.to_string())
2017-03-02 14:37:32 +00:00
.put_attribute("version", "1.0")
.put_attribute("xmlns", "jabber:client")
.put_attribute("stream", "http://etherx.jabber.org/streams", XMLNS_URI);
outs.has_nodes = true;
2017-04-03 13:09:30 +00:00
log.node("OUT ROOT", outs);
2017-03-02 14:37:32 +00:00
write(outs);
2017-11-11 20:29:13 +00:00
received_root_node(this, yield read_root());
2017-03-02 14:37:32 +00:00
}
2017-11-11 20:29:13 +00:00
private async void loop() throws IOStreamError {
2017-04-03 13:09:30 +00:00
while (true) {
2017-03-02 14:37:32 +00:00
if (setup_needed) {
2017-11-11 20:29:13 +00:00
yield setup();
2017-03-02 14:37:32 +00:00
setup_needed = false;
}
2017-11-11 20:29:13 +00:00
StanzaNode node = yield read();
Idle.add(loop.callback);
yield;
if (disconnected) break;
2017-03-02 14:37:32 +00:00
received_node(this, node);
if (node.ns_uri == NS_URI && node.name == "features") {
features = node;
received_features_node(this);
} else if (node.ns_uri == NS_URI && node.name == "stream" && node.pseudo) {
2019-03-15 19:56:19 +00:00
debug("[%p] Server closed stream", this);
try {
yield disconnect();
} catch (Error e) {}
2017-03-02 14:37:32 +00:00
return;
} else if (node.ns_uri == JABBER_URI) {
if (node.name == "message") {
received_message_stanza(this, node);
} else if (node.name == "presence") {
received_presence_stanza(this, node);
} else if (node.name == "iq") {
received_iq_stanza(this, node);
} else {
received_nonza(this, node);
}
} else {
received_nonza(this, node);
}
2017-08-12 21:14:50 +00:00
if (!non_negotiation_modules_attached && negotiation_modules_done()) {
2017-03-02 14:37:32 +00:00
attach_non_negotation_modules();
2017-08-12 21:14:50 +00:00
non_negotiation_modules_attached = true;
if (!negotiation_complete) {
stream_negotiated(this);
negotiation_complete = true;
}
2017-03-02 14:37:32 +00:00
}
}
}
private bool negotiation_modules_done() throws IOStreamError {
2018-01-04 20:13:44 +00:00
if (setup_needed) return false;
if (is_negotiation_active()) return false;
foreach (XmppStreamModule module in modules) {
if (module is XmppStreamNegotiationModule) {
XmppStreamNegotiationModule negotiation_module = (XmppStreamNegotiationModule) module;
if (negotiation_module.mandatory_outstanding(this)) {
throw new IOStreamError.CONNECT("mandatory-to-negotiate feature not negotiated: " + negotiation_module.get_id());
2017-03-02 14:37:32 +00:00
}
}
}
2018-01-04 20:13:44 +00:00
return true;
2017-03-02 14:37:32 +00:00
}
private void attach_non_negotation_modules() {
foreach (XmppStreamModule module in modules) {
if (module as XmppStreamNegotiationModule == null) {
module.attach(this);
}
2017-08-12 21:14:50 +00:00
}
attached_modules(this);
}
private void attach_negotation_modules() {
foreach (XmppStreamModule module in modules) {
if (module as XmppStreamNegotiationModule != null) {
module.attach(this);
}
2017-03-02 14:37:32 +00:00
}
}
2017-11-11 20:29:13 +00:00
private async StanzaNode read_root() throws IOStreamError {
StanzaReader? reader = this.reader;
if (reader == null) throw new IOStreamError.READ("trying to read, but no stream open");
2017-03-02 14:37:32 +00:00
try {
2017-11-11 20:29:13 +00:00
StanzaNode node = yield ((!)reader).read_root_node();
2017-04-03 13:09:30 +00:00
log.node("IN ROOT", node);
2017-03-02 14:37:32 +00:00
return node;
2018-01-04 20:13:44 +00:00
} catch (XmlError.TLS e) {
throw new IOStreamError.TLS(e.message);
} catch (Error e) {
2017-03-02 14:37:32 +00:00
throw new IOStreamError.READ(e.message);
}
}
}
public class FlagIdentity<T> : Object {
public string ns { get; private set; }
public string id { get; private set; }
public FlagIdentity(string ns, string id) {
this.ns = ns;
this.id = id;
}
public T? cast(XmppStreamFlag flag) {
return flag.get_type().is_a(typeof(T)) ? (T?) flag : null;
}
public bool matches(XmppStreamFlag module) {
return module.get_ns() == ns && module.get_id() == id;
}
}
public abstract class XmppStreamFlag : Object {
2017-03-10 15:16:48 +00:00
public abstract string get_ns();
2017-04-03 13:09:30 +00:00
2017-03-10 15:16:48 +00:00
public abstract string get_id();
2017-03-02 14:37:32 +00:00
}
2017-03-10 20:13:35 +00:00
public class ModuleIdentity<T> : Object {
public string ns { get; private set; }
public string id { get; private set; }
public ModuleIdentity(string ns, string id) {
this.ns = ns;
this.id = id;
}
public T? cast(XmppStreamModule module) {
return module.get_type().is_a(typeof(T)) ? (T?) module : null;
2017-03-10 20:13:35 +00:00
}
2017-03-10 20:45:56 +00:00
public bool matches(XmppStreamModule module) {
return module.get_ns() == ns && module.get_id() == id;
}
2017-03-10 20:13:35 +00:00
}
2017-03-02 14:37:32 +00:00
public abstract class XmppStreamModule : Object {
2017-03-10 15:16:48 +00:00
public abstract void attach(XmppStream stream);
2017-04-03 13:09:30 +00:00
2017-03-10 15:16:48 +00:00
public abstract void detach(XmppStream stream);
2017-04-03 13:09:30 +00:00
2017-03-10 15:16:48 +00:00
public abstract string get_ns();
2017-04-03 13:09:30 +00:00
2017-03-10 15:16:48 +00:00
public abstract string get_id();
2017-03-02 14:37:32 +00:00
}
public abstract class XmppStreamNegotiationModule : XmppStreamModule {
2017-03-10 15:16:48 +00:00
public abstract bool mandatory_outstanding(XmppStream stream);
2017-04-03 13:09:30 +00:00
2017-03-10 15:16:48 +00:00
public abstract bool negotiation_active(XmppStream stream);
2017-03-02 14:37:32 +00:00
}
2017-04-03 13:09:30 +00:00
2017-08-09 18:44:15 +00:00
public abstract class ConnectionProvider {
public async abstract int? get_priority(Jid remote_name);
public async abstract IOStream? connect(XmppStream stream);
2017-08-09 18:44:15 +00:00
public abstract string get_id();
}
public class StartTlsConnectionProvider : ConnectionProvider {
private SrvTarget? srv_target;
public async override int? get_priority(Jid remote_name) {
2017-08-09 18:44:15 +00:00
GLib.List<SrvTarget>? xmpp_target = null;
try {
GLibFixes.Resolver resolver = GLibFixes.Resolver.get_default();
xmpp_target = yield resolver.lookup_service_async("xmpp-client", "tcp", remote_name.to_string(), null);
2017-08-09 18:44:15 +00:00
} catch (Error e) {
return null;
}
xmpp_target.sort((a, b) => { return a.get_priority() - b.get_priority(); });
srv_target = xmpp_target.nth(0).data;
return xmpp_target.nth(0).data.get_priority();
}
public async override IOStream? connect(XmppStream stream) {
2017-10-29 14:15:28 +00:00
try {
SocketClient client = new SocketClient();
debug("Connecting to %s %i (starttls)", srv_target.get_hostname(), srv_target.get_port());
return yield client.connect_to_host_async(srv_target.get_hostname(), srv_target.get_port());
2017-10-29 14:15:28 +00:00
} catch (Error e) {
return null;
}
2017-08-09 18:44:15 +00:00
}
public override string get_id() { return "start_tls"; }
}
2017-03-02 14:37:32 +00:00
}