aboutsummaryrefslogtreecommitdiff
path: root/xmpp-vala/src/module/xep/0166_jingle/jingle_module.vala
blob: 186848f6ff6742a2c297342d11bae6f2d8743796 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
using Gee;
using Xmpp;

namespace Xmpp.Xep.Jingle {

    public const string NS_URI = "urn:xmpp:jingle:1";
    private const string ERROR_NS_URI = "urn:xmpp:jingle:errors:1";

    // This module can only be attached to one stream at a time.
    public class Module : XmppStreamModule, Iq.Handler {
        public static Xmpp.ModuleIdentity<Module> IDENTITY = new Xmpp.ModuleIdentity<Module>(NS_URI, "0166_jingle");

        public signal void session_initiate_received(XmppStream stream, Session session);

        private HashMap<string, ContentType> content_types = new HashMap<string, ContentType>();
        private HashMap<string, SessionInfoNs> session_info_types = new HashMap<string, SessionInfoNs>();
        private HashMap<string, Transport> transports = new HashMap<string, Transport>();
        private HashMap<string, SecurityPrecondition> security_preconditions = new HashMap<string, SecurityPrecondition>();

        public override void attach(XmppStream stream) {
            stream.add_flag(new Flag());
            stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI);
            stream.get_module(Iq.Module.IDENTITY).register_for_namespace(NS_URI, this);

            // TODO update stream in all sessions
        }

        public override void detach(XmppStream stream) {
            stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI);
            stream.get_module(Iq.Module.IDENTITY).unregister_from_namespace(NS_URI, this);
        }

        public void register_content_type(ContentType content_type) {
            content_types[content_type.ns_uri] = content_type;
        }

        public void register_session_info_type(SessionInfoNs info_ns) {
            session_info_types[info_ns.ns_uri] = info_ns;
        }

        public ContentType? get_content_type(string ns_uri) {
            if (!content_types.has_key(ns_uri)) {
                return null;
            }
            return content_types[ns_uri];
        }

        public SessionInfoNs? get_session_info_type(string ns_uri) {
            return session_info_types[ns_uri];
        }

        public void register_transport(Transport transport) {
            transports[transport.ns_uri] = transport;
        }

        public Transport? get_transport(string ns_uri) {
            if (!transports.has_key(ns_uri)) {
                return null;
            }
            return transports[ns_uri];
        }

        public async Transport? select_transport(XmppStream stream, TransportType type, uint8 components, Jid receiver_full_jid, Set<string> blacklist) {
            Transport? result = null;
            foreach (Transport transport in transports.values) {
                if (transport.type_ != type) {
                    continue;
                }
                if (transport.ns_uri in blacklist) {
                    continue;
                }
                if (yield transport.is_transport_available(stream, components, receiver_full_jid)) {
                    if (result != null) {
                        if (result.priority >= transport.priority) {
                            continue;
                        }
                    }
                    result = transport;
                }
            }
            return result;
        }

        public void register_security_precondition(SecurityPrecondition precondition) {
            security_preconditions[precondition.security_ns_uri()] = precondition;
        }

        public SecurityPrecondition? get_security_precondition(string? ns_uri) {
            if (ns_uri == null) return null;
            if (!security_preconditions.has_key(ns_uri)) {
                return null;
            }
            return security_preconditions[ns_uri];
        }

        private async bool is_jingle_available(XmppStream stream, Jid full_jid) {
            bool? has_jingle = yield stream.get_module(ServiceDiscovery.Module.IDENTITY).has_entity_feature(stream, full_jid, NS_URI);
            return has_jingle != null && has_jingle;
        }

        public async bool is_available(XmppStream stream, TransportType type, uint8 components, Jid full_jid) {
            return (yield is_jingle_available(stream, full_jid)) && (yield select_transport(stream, type, components, full_jid, Set.empty())) != null;
        }

        public async Session create_session(XmppStream stream, Gee.List<Content> contents, Jid receiver_full_jid, string? sid = null) throws Error {
            if (!yield is_jingle_available(stream, receiver_full_jid)) {
                throw new Error.NO_SHARED_PROTOCOLS("No Jingle support");
            }
            Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid;
            if (my_jid == null) {
                throw new Error.GENERAL("Couldn't determine own JID");
            }

            Session session = new Session.initiate_sent(stream, sid ?? random_uuid(), my_jid, receiver_full_jid);
            session.terminated.connect((session, stream, _1, _2, _3) => { stream.get_flag(Flag.IDENTITY).remove_session(session.sid); });

            foreach (Content content in contents) {
                session.insert_content(content);
            }

            // Build & send session-initiate iq stanza
            StanzaNode initiate_jingle_iq = new StanzaNode.build("jingle", NS_URI)
                    .add_self_xmlns()
                    .put_attribute("action", "session-initiate")
                    .put_attribute("initiator", my_jid.to_string())
                    .put_attribute("sid", session.sid);

            foreach (Content content in contents) {
                StanzaNode content_node = new StanzaNode.build("content", NS_URI)
                        .put_attribute("creator", "initiator")
                        .put_attribute("name", content.content_name)
                        .put_attribute("senders", content.senders.to_string())
                        .put_node(content.content_params.get_description_node())
                        .put_node(content.transport_params.to_transport_stanza_node("session-initiate"));
                if (content.security_params != null) {
                    content_node.put_node(content.security_params.to_security_stanza_node(stream, my_jid, receiver_full_jid));
                }
                initiate_jingle_iq.put_node(content_node);
            }

            Iq.Stanza iq = new Iq.Stanza.set(initiate_jingle_iq) { to=receiver_full_jid };

            stream.get_flag(Flag.IDENTITY).add_session(session);
            // We might get a follow-up before the ack => add_session before send_iq returns
            stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq, (stream, iq) => {
                if (iq.is_error()) warning("Jingle session-initiate got error: %s", iq.stanza.to_string());
            });

            return session;
        }

        public async void handle_session_initiate(XmppStream stream, string sid, StanzaNode jingle, Iq.Stanza iq) throws IqError {
            Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid;
            if (my_jid == null) {
                throw new IqError.RESOURCE_CONSTRAINT("Couldn't determine own JID");
            }

            Session session = new Session.initiate_received(stream, sid, my_jid, iq.from);
            session.terminated.connect((stream) => { stream.get_flag(Flag.IDENTITY).remove_session(sid); });

            stream.get_flag(Flag.IDENTITY).pre_add_session(session.sid);

            foreach (ContentNode content_node in get_content_nodes(jingle)) {
                yield session.insert_content_node(content_node, iq.from);
            }

            stream.get_flag(Flag.IDENTITY).add_session(session);

            stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq));

            session_initiate_received(stream, session);
        }

        public async void on_iq_set(XmppStream stream, Iq.Stanza iq) {
            try {
                yield handle_iq_set(stream, iq);
            } catch (IqError e) {
                send_iq_error(e, stream, iq);
            }
        }

        public async void handle_iq_set(XmppStream stream, Iq.Stanza iq) throws IqError {
            StanzaNode? jingle_node = iq.stanza.get_subnode("jingle", NS_URI);
            if (jingle_node == null) {
                throw new IqError.BAD_REQUEST("missing jingle node");
            }
            string? sid = jingle_node.get_attribute("sid");
            string? action = jingle_node.get_attribute("action");
            if (sid == null || action == null) {
                throw new IqError.BAD_REQUEST("missing jingle node, sid or action");
            }

            Session? session = yield stream.get_flag(Flag.IDENTITY).get_session(sid);
            if (action == "session-initiate") {
                if (session != null) {
                    stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.error(iq, new ErrorStanza.build(ErrorStanza.TYPE_MODIFY, ErrorStanza.CONDITION_CONFLICT, "session ID already in use", null)) { to=iq.from });
                    return;
                }
                yield handle_session_initiate(stream, sid, jingle_node, iq);
                return;
            }
            if (session == null) {
                StanzaNode unknown_session = new StanzaNode.build("unknown-session", ERROR_NS_URI).add_self_xmlns();
                stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.error(iq, new ErrorStanza.item_not_found(unknown_session)) { to=iq.from });
                return;
            }
            session.handle_iq_set(action, jingle_node, iq);
        }

        public override string get_ns() { return NS_URI; }
        public override string get_id() { return IDENTITY.id; }
    }

    void send_iq_error(IqError iq_error, XmppStream stream, Iq.Stanza iq) {
        ErrorStanza error;
        if (iq_error is IqError.BAD_REQUEST) {
            error = new ErrorStanza.bad_request(iq_error.message);
        } else if (iq_error is IqError.NOT_ACCEPTABLE) {
            error = new ErrorStanza.not_acceptable(iq_error.message);
        } else if (iq_error is IqError.NOT_IMPLEMENTED) {
            error = new ErrorStanza.feature_not_implemented(iq_error.message);
        } else if (iq_error is IqError.UNSUPPORTED_INFO) {
            StanzaNode unsupported_info = new StanzaNode.build("unsupported-info", ERROR_NS_URI).add_self_xmlns();
            error = new ErrorStanza.build(ErrorStanza.TYPE_CANCEL, ErrorStanza.CONDITION_FEATURE_NOT_IMPLEMENTED, iq_error.message, unsupported_info);
        } else if (iq_error is IqError.OUT_OF_ORDER) {
            StanzaNode out_of_order = new StanzaNode.build("out-of-order", ERROR_NS_URI).add_self_xmlns();
            error = new ErrorStanza.build(ErrorStanza.TYPE_MODIFY, ErrorStanza.CONDITION_UNEXPECTED_REQUEST, iq_error.message, out_of_order);
        } else if (iq_error is IqError.RESOURCE_CONSTRAINT) {
            error = new ErrorStanza.resource_constraint(iq_error.message);
        } else {
            assert_not_reached();
        }
        stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.error(iq, error) { to=iq.from });
    }
}