1using Gee;
2using Xmpp;
3using Xmpp.Xep;
4
5namespace Xmpp.Xep.JingleFileTransfer {
6
7private const string NS_URI = "urn:xmpp:jingle:apps:file-transfer:5";
8
9public class Module : Jingle.ContentType, XmppStreamModule {
10    public static Xmpp.ModuleIdentity<Module> IDENTITY = new Xmpp.ModuleIdentity<Module>(NS_URI, "0234_jingle_file_transfer");
11
12    public override void attach(XmppStream stream) {
13        stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI);
14        stream.get_module(Jingle.Module.IDENTITY).register_content_type(this);
15    }
16    public override void detach(XmppStream stream) {
17        stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI);
18    }
19
20    public string content_type_ns_uri() {
21        return NS_URI;
22    }
23    public Jingle.TransportType content_type_transport_type() {
24        return Jingle.TransportType.STREAMING;
25    }
26    public Jingle.ContentParameters parse_content_parameters(StanzaNode description) throws Jingle.IqError {
27        return Parameters.parse(this, description);
28    }
29    public void handle_content_session_info(XmppStream stream, Jingle.Session session, StanzaNode info, Iq.Stanza iq) throws Jingle.IqError {
30        switch (info.name) {
31            case "received":
32                stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq));
33                break;
34            case "checksum":
35                // TODO(hrxi): handle hash
36                stream.get_module(Iq.Module.IDENTITY).send_iq(stream, new Iq.Stanza.result(iq));
37                break;
38            default:
39                throw new Jingle.IqError.UNSUPPORTED_INFO(@"unsupported file transfer info $(info.name)");
40        }
41    }
42
43    public signal void file_incoming(XmppStream stream, FileTransfer file_transfer);
44
45    public async bool is_available(XmppStream stream, Jid full_jid) {
46        bool? has_feature = yield stream.get_module(ServiceDiscovery.Module.IDENTITY).has_entity_feature(stream, full_jid, NS_URI);
47        if (has_feature == null || !(!)has_feature) {
48            return false;
49        }
50        return yield stream.get_module(Jingle.Module.IDENTITY).is_available(stream, Jingle.TransportType.STREAMING, full_jid);
51    }
52
53    public async void offer_file_stream(XmppStream stream, Jid receiver_full_jid, InputStream input_stream, string basename, int64 size, string? precondition_name = null, Object? precondition_options = null) throws IOError {
54        StanzaNode file_node;
55        StanzaNode description = new StanzaNode.build("description", NS_URI)
56            .add_self_xmlns()
57            .put_node(file_node = new StanzaNode.build("file", NS_URI)
58                .put_node(new StanzaNode.build("name", NS_URI).put_node(new StanzaNode.text(basename))));
59                // TODO(hrxi): Add the mandatory hash field
60
61        if (size > 0) {
62            file_node.put_node(new StanzaNode.build("size", NS_URI).put_node(new StanzaNode.text(size.to_string())));
63        } else {
64            warning("Sending file %s without size, likely going to cause problems down the road...", basename);
65        }
66
67        Jingle.Session session;
68        try {
69            session = yield stream.get_module(Jingle.Module.IDENTITY)
70                .create_session(stream, Jingle.TransportType.STREAMING, receiver_full_jid, Jingle.Senders.INITIATOR, "a-file-offer", description, precondition_name, precondition_options); // TODO(hrxi): Why "a-file-offer"?
71        } catch (Jingle.Error e) {
72            throw new IOError.FAILED(@"couldn't create Jingle session: $(e.message)");
73        }
74        session.terminate_on_connection_close = false;
75
76        yield session.conn.input_stream.close_async();
77
78        // TODO(hrxi): catch errors
79        yield session.conn.output_stream.splice_async(input_stream, OutputStreamSpliceFlags.CLOSE_SOURCE|OutputStreamSpliceFlags.CLOSE_TARGET);
80    }
81
82    public override string get_ns() { return NS_URI; }
83    public override string get_id() { return IDENTITY.id; }
84}
85
86public class Parameters : Jingle.ContentParameters, Object {
87
88    Module parent;
89    string? media_type;
90    public string? name { get; private set; }
91    public int64 size { get; private set; }
92    public StanzaNode original_description { get; private set; }
93
94    public Parameters(Module parent, StanzaNode original_description, string? media_type, string? name, int64 size) {
95        this.parent = parent;
96        this.original_description = original_description;
97        this.media_type = media_type;
98        this.name = name;
99        this.size = size;
100    }
101
102    public static Parameters parse(Module parent, StanzaNode description) throws Jingle.IqError {
103        Gee.List<StanzaNode> files = description.get_subnodes("file", NS_URI);
104        if (files.size != 1) {
105            throw new Jingle.IqError.BAD_REQUEST("there needs to be exactly one file node");
106        }
107        StanzaNode file = files[0];
108        StanzaNode? media_type_node = file.get_subnode("media-type", NS_URI);
109        StanzaNode? name_node = file.get_subnode("name", NS_URI);
110        StanzaNode? size_node = file.get_subnode("size", NS_URI);
111        string? media_type = media_type_node != null ? media_type_node.get_string_content() : null;
112        string? name = name_node != null ? name_node.get_string_content() : null;
113        string? size_raw = size_node != null ? size_node.get_string_content() : null;
114        // TODO(hrxi): For some reason, the ?:-expression does not work due to a type error.
115        //int64? size = size_raw != null ? int64.parse(size_raw) : null; // TODO(hrxi): this has no error handling
116        if (size_raw == null) {
117            // Jingle file transfers (XEP-0234) theoretically SHOULD send a
118            // file size, however, we do require it in order to reliably find
119            // the end of the file transfer.
120            throw new Jingle.IqError.BAD_REQUEST("file offer without file size");
121        }
122        int64 size = int64.parse(size_raw);
123        if (size < 0) {
124            throw new Jingle.IqError.BAD_REQUEST("negative file size is invalid");
125        }
126
127        return new Parameters(parent, description, media_type, name, size);
128    }
129
130    public void on_session_initiate(XmppStream stream, Jingle.Session session) {
131        parent.file_incoming(stream, new FileTransfer(session, this));
132    }
133}
134
135// Does nothing except wrapping an input stream to signal EOF after reading
136// `max_size` bytes.
137private class FileTransferInputStream : InputStream {
138    InputStream inner;
139    int64 remaining_size;
140    public FileTransferInputStream(InputStream inner, int64 max_size) {
141        this.inner = inner;
142        this.remaining_size = max_size;
143    }
144    private ssize_t update_remaining(ssize_t read) {
145        this.remaining_size -= read;
146        return read;
147    }
148    public override ssize_t read(uint8[] buffer_, Cancellable? cancellable = null) throws IOError {
149        unowned uint8[] buffer = buffer_;
150        if (remaining_size <= 0) {
151            return 0;
152        }
153        if (buffer.length > remaining_size) {
154            buffer = buffer[0:remaining_size];
155        }
156        return update_remaining(inner.read(buffer, cancellable));
157    }
158    public override async ssize_t read_async(uint8[]? buffer_, int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError {
159        unowned uint8[] buffer = buffer_;
160        if (remaining_size <= 0) {
161            return 0;
162        }
163        if (buffer.length > remaining_size) {
164            buffer = buffer[0:remaining_size];
165        }
166        return update_remaining(yield inner.read_async(buffer, io_priority, cancellable));
167    }
168    public override bool close(Cancellable? cancellable = null) throws IOError {
169        return inner.close(cancellable);
170    }
171    public override async bool close_async(int io_priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws IOError {
172        return yield inner.close_async(io_priority, cancellable);
173    }
174}
175
176public class FileTransfer : Object {
177    Jingle.Session session;
178    Parameters parameters;
179
180    public Jid peer { get { return session.peer_full_jid; } }
181    public string? file_name { get { return parameters.name; } }
182    public int64 size { get { return parameters.size; } }
183    public Jingle.SecurityParameters? security { get { return session.security; } }
184
185    public InputStream? stream { get; private set; }
186
187    public FileTransfer(Jingle.Session session, Parameters parameters) {
188        this.session = session;
189        this.parameters = parameters;
190        this.stream = new FileTransferInputStream(session.conn.input_stream, size);
191    }
192
193    public void accept(XmppStream stream) throws IOError {
194        session.accept(stream, parameters.original_description);
195        session.conn.output_stream.close();
196    }
197
198    public void reject(XmppStream stream) {
199        session.reject(stream);
200    }
201}
202
203}
204