aboutsummaryrefslogtreecommitdiff
path: root/xmpp-vala/src/module/xep/0166_jingle.vala
blob: 5c086399626217cca3f742c2623f83530d068df6 (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
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
using Gee;
using Xmpp.Xep;
using Xmpp;

namespace Xmpp.Xep.Jingle {

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

public errordomain CreateConnectionError {
    BAD_REQUEST,
    NOT_ACCEPTABLE,
}

public errordomain Error {
    GENERAL,
    BAD_REQUEST,
    INVALID_PARAMETERS,
    UNSUPPORTED_TRANSPORT,
    NO_SHARED_PROTOCOLS,
    TRANSPORT_ERROR,
}

public class Module : XmppStreamModule, Iq.Handler {
    public static Xmpp.ModuleIdentity<Module> IDENTITY = new Xmpp.ModuleIdentity<Module>(NS_URI, "0166_jingle");

    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);
    }
    public override void detach(XmppStream stream) { }

    public void add_transport(XmppStream stream, Transport transport) {
        stream.get_flag(Flag.IDENTITY).add_transport(transport);
    }
    public Transport? select_transport(XmppStream stream, TransportType type, Jid receiver_full_jid) {
        return stream.get_flag(Flag.IDENTITY).select_transport(stream, type, receiver_full_jid);
    }

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

    public bool is_available(XmppStream stream, TransportType type, Jid full_jid) {
        return is_jingle_available(stream, full_jid) && select_transport(stream, type, full_jid) != null;
    }

    public Session create_session(XmppStream stream, TransportType type, Jid receiver_full_jid, Senders senders, string content_name, StanzaNode description) throws Error {
        if (!is_jingle_available(stream, receiver_full_jid)) {
            throw new Error.NO_SHARED_PROTOCOLS("No Jingle support");
        }
        Transport? transport = select_transport(stream, type, receiver_full_jid);
        if (transport == null) {
            throw new Error.NO_SHARED_PROTOCOLS("No suitable transports");
        }
        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(random_uuid(), type, receiver_full_jid);
        StanzaNode content = new StanzaNode.build("content", NS_URI)
            .put_attribute("creator", "initiator")
            .put_attribute("name", content_name)
            .put_attribute("senders", senders.to_string())
            .put_node(description)
            .put_node(transport.to_transport_stanza_node());
        StanzaNode jingle = 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)
            .put_node(content);
        Iq.Stanza iq = new Iq.Stanza.set(jingle) { to=receiver_full_jid };

        stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq, (stream, iq) => {
            stream.get_flag(Flag.IDENTITY).add_session(session);
        });

        return session;
    }

    public void on_iq_set(XmppStream stream, Iq.Stanza iq) {
        StanzaNode? jingle = iq.stanza.get_subnode("jingle", NS_URI);
        string? sid = jingle != null ? jingle.get_attribute("sid") : null;
        string? action = jingle != null ? jingle.get_attribute("action") : null;
        if (jingle == null || sid == null || action == null) {
            stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.error(iq, new ErrorStanza.bad_request("missing jingle node, sid or action")));
            return;
        }
        Session? session = stream.get_flag(Flag.IDENTITY).get_session(sid);
        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)));
            return;
        }
        session.handle_iq_set(stream, action, jingle, iq);
    }

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

public enum TransportType {
    DATAGRAM,
    STREAMING,
}

public enum Senders {
    BOTH,
    INITIATOR,
    NONE,
    RESPONDER;

    public string to_string() {
        switch (this) {
            case BOTH: return "both";
            case INITIATOR: return "initiator";
            case NONE: return "none";
            case RESPONDER: return "responder";
        }
        assert_not_reached();
    }
}

public interface Transport : Object {
    public abstract bool is_transport_available(XmppStream stream, Jid full_jid);
    public abstract TransportType transport_type();
    public abstract StanzaNode to_transport_stanza_node();
    public abstract Connection? create_transport_connection(XmppStream stream, Jid peer_full_jid, StanzaNode content) throws CreateConnectionError;
}

public class Session {
    public enum State {
        PENDING,
        ACTIVE,
        ENDED,
    }

    public State state { get; private set; }
    Connection? conn;

    public string sid { get; private set; }
    public Type type_ { get; private set; }
    public Jid peer_full_jid { get; private set; }

    public Session(string sid, Type type, Jid peer_full_jid) {
        this.state = PENDING;
        this.conn = null;
        this.sid = sid;
        this.type_ = type;
        this.peer_full_jid = peer_full_jid;
    }

    public signal void on_error(XmppStream stream, Error error);
    public signal void on_data(XmppStream stream, uint8[] data);
    // Signals that the stream is ready to send (more) data.
    public signal void on_ready(XmppStream stream);

    private void handle_error(XmppStream stream, Error error) {
        if (state == PENDING || state == ACTIVE) {
            StanzaNode reason = new StanzaNode.build("reason", NS_URI)
                .put_node(new StanzaNode.build("general-error", NS_URI)) // TODO(hrxi): Is this the right error?
                .put_node(new StanzaNode.build("text", NS_URI)
                    .put_node(new StanzaNode.text(error.message))
                );
            terminate(stream, reason);
        }
    }

    delegate void SendIq(Iq.Stanza iq);
    public void handle_iq_set(XmppStream stream, string action, StanzaNode jingle, Iq.Stanza iq) {
        SendIq send_iq = (iq) => stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq);
        if (state != PENDING || action != "session-accept") {
            return;
        }
        StanzaNode? content = jingle.get_subnode("content");
        if (content == null) {
            // TODO(hrxi): here and below, should we terminate the session?
            send_iq(new Iq.Stanza.error(iq, new ErrorStanza.bad_request("no content element")));
            return;
        }
        string? responder_str = jingle.get_attribute("responder");
        Jid responder;
        if (responder_str != null) {
            responder = Jid.parse(responder_str) ?? iq.from;
        } else {
            responder = iq.from; // TODO(hrxi): and above, can we assume iq.from != null
            // TODO(hrxi): more sanity checking, perhaps replace who we're talking to
        }
        if (!responder.is_full()) {
            send_iq(new Iq.Stanza.error(iq, new ErrorStanza.bad_request("invalid responder JID")));
            return;
        }
        try {
            conn = stream.get_flag(Flag.IDENTITY).create_connection(stream, type_, peer_full_jid, content);
        } catch (CreateConnectionError e) {
            if (e is CreateConnectionError.BAD_REQUEST) {
                send_iq(new Iq.Stanza.error(iq, new ErrorStanza.bad_request(e.message)));
            } else if (e is CreateConnectionError.NOT_ACCEPTABLE) {
                send_iq(new Iq.Stanza.error(iq, new ErrorStanza.not_acceptable(e.message)));
            }
            return;
        }
        send_iq(new Iq.Stanza.result(iq));
        if (conn == null) {
            terminate(stream, new StanzaNode.build("reason", NS_URI)
                .put_node(new StanzaNode.build("unsupported-transports", NS_URI)));
            return;
        }
        conn.on_error.connect((stream, error) => on_error(stream, error));
        conn.on_data.connect((stream, data) => on_data(stream, data));
        conn.on_ready.connect((stream) => on_ready(stream));
        on_error.connect((stream, error) => handle_error(stream, error));
        conn.connect(stream);
        state = ACTIVE;
    }

    public void send(XmppStream stream, uint8[] data) {
        if (state != ACTIVE) {
            return; // TODO(hrxi): what to do?
        }
        conn.send(stream, data);
    }

    public void set_application_error(XmppStream stream, StanzaNode? application_reason = null) {
        StanzaNode reason = new StanzaNode.build("reason", NS_URI)
            .put_node(new StanzaNode.build("failed-application", NS_URI));
        if (application_reason != null) {
            reason.put_node(application_reason);
        }
        terminate(stream, reason);
    }

    public void close_connection(XmppStream stream) {
        if (state != ACTIVE) {
            return; // TODO(hrxi): what to do?
        }
        conn.close(stream);
    }

    public void terminate(XmppStream stream, StanzaNode reason) {
        if (state != PENDING && state != ACTIVE) {
            // TODO(hrxi): what to do?
            return;
        }
        if (conn != null) {
            conn.close(stream);
        }

        StanzaNode jingle = new StanzaNode.build("jingle", NS_URI)
            .add_self_xmlns()
            .put_attribute("action", "session-terminate")
            .put_attribute("sid", sid)
            .put_node(reason);
        Iq.Stanza iq = new Iq.Stanza.set(jingle) { to=peer_full_jid };
        stream.get_module(Iq.Module.IDENTITY).send_iq(stream, iq);

        state = ENDED;
        // Immediately remove the session from the open sessions as per the
        // XEP, don't wait for confirmation.
        stream.get_flag(Flag.IDENTITY).remove_session(sid);
    }
}

public abstract class Connection {
    public Jid? peer_full_jid { get; private set; }

    public Connection(Jid peer_full_jid) {
        this.peer_full_jid = peer_full_jid;
    }

    public signal void on_error(XmppStream stream, Error error);
    public signal void on_data(XmppStream stream, uint8[] data);
    public signal void on_ready(XmppStream stream);

    public abstract void connect(XmppStream stream);
    public abstract void send(XmppStream stream, uint8[] data);
    public abstract void close(XmppStream stream);
}

public class Flag : XmppStreamFlag {
    public static FlagIdentity<Flag> IDENTITY = new FlagIdentity<Flag>(NS_URI, "jingle");

    private Gee.List<Transport> transports = new ArrayList<Transport>();
    private HashMap<string, Session> sessions = new HashMap<string, Session>();

    public void add_transport(Transport transport) { transports.add(transport); }
    public Transport? select_transport(XmppStream stream, TransportType type, Jid receiver_full_jid) {
        foreach (Transport transport in transports) {
            if (transport.transport_type() != type) {
                continue;
            }
            // TODO(hrxi): prioritization
            if (transport.is_transport_available(stream, receiver_full_jid)) {
                return transport;
            }
        }
        return null;
    }
    public void add_session(Session session) {
        sessions[session.sid] = session;
    }
    public Connection? create_connection(XmppStream stream, Type type, Jid peer_full_jid, StanzaNode content) throws CreateConnectionError {
        foreach (Transport transport in transports) {
            if (transport.transport_type() != type) {
                continue;
            }
            Connection? conn = transport.create_transport_connection(stream, peer_full_jid, content);
            if (conn != null) {
                return conn;
            }
        }
        return null;
    }
    public Session? get_session(string sid) {
        return sessions.has_key(sid) ? sessions[sid] : null;
    }
    public void remove_session(string sid) {
        sessions.unset(sid);
    }

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

}