aboutsummaryrefslogtreecommitdiff
path: root/libdino/src/service/jingle_file_transfers.vala
blob: c607b6c6424629f32d02585d5876513ff99201be (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
using Gdk;
using Gee;

using Xmpp;
using Dino.Entities;

namespace Dino {

public interface JingleFileEncryptionHelper : Object {
    public abstract bool can_transfer(Conversation conversation);
    public abstract async bool can_encrypt(Conversation conversation, FileTransfer file_transfer, Jid? full_jid = null);
    public abstract string? get_precondition_name(Conversation conversation, FileTransfer file_transfer);
    public abstract Object? get_precondition_options(Conversation conversation, FileTransfer file_transfer);
    public abstract Encryption get_encryption(Xmpp.Xep.JingleFileTransfer.FileTransfer jingle_transfer);
}

public class JingleFileEncryptionHelperTransferOnly : JingleFileEncryptionHelper, Object  {
    public bool can_transfer(Conversation conversation) {
        return true;
    }
    public async bool can_encrypt(Conversation conversation, FileTransfer file_transfer, Jid? full_jid) {
        return false;
    }
    public string? get_precondition_name(Conversation conversation, FileTransfer file_transfer) {
        return null;
    }
    public Object? get_precondition_options(Conversation conversation, FileTransfer file_transfer) {
        return null;
    }
    public Encryption get_encryption(Xmpp.Xep.JingleFileTransfer.FileTransfer jingle_transfer) {
        return Encryption.NONE;
    }
}

public class JingleFileHelperRegistry {
    private static JingleFileHelperRegistry INSTANCE;
    public static JingleFileHelperRegistry instance { get {
        if (INSTANCE == null) {
            INSTANCE = new JingleFileHelperRegistry();
            INSTANCE.add_encryption_helper(Encryption.NONE, new JingleFileEncryptionHelperTransferOnly());
        }
        return INSTANCE;
    } }

    internal HashMap<Encryption, JingleFileEncryptionHelper> encryption_helpers = new HashMap<Encryption, JingleFileEncryptionHelper>();

    public void add_encryption_helper(Encryption encryption, JingleFileEncryptionHelper helper) {
        encryption_helpers[encryption] = helper;
    }

    public JingleFileEncryptionHelper? get_encryption_helper(Encryption encryption) {
        if (encryption_helpers.has_key(encryption)) {
            return encryption_helpers[encryption];
        }
        return null;
    }
}

public class JingleFileProvider : FileProvider, Object {

    private StreamInteractor stream_interactor;
    private HashMap<string, Xmpp.Xep.JingleFileTransfer.FileTransfer> file_transfers = new HashMap<string, Xmpp.Xep.JingleFileTransfer.FileTransfer>();

    public JingleFileProvider(StreamInteractor stream_interactor) {
        this.stream_interactor = stream_interactor;

        stream_interactor.stream_negotiated.connect(on_stream_negotiated);
    }

    public FileMeta get_file_meta(FileTransfer file_transfer) throws FileReceiveError {
        var file_meta = new FileMeta();
        file_meta.file_name = file_transfer.file_name;
        file_meta.size = file_transfer.size;
        return file_meta;
    }

    public FileReceiveData? get_file_receive_data(FileTransfer file_transfer) {
        return new FileReceiveData();
    }

    public async FileMeta get_meta_info(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws FileReceiveError {
        return file_meta;
    }

    public Encryption get_encryption(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) {
        Xmpp.Xep.JingleFileTransfer.FileTransfer? jingle_file_transfer = file_transfers[file_transfer.info];
        if (jingle_file_transfer == null) {
            warning("Could not determine jingle encryption - transfer data not available anymore");
            return Encryption.NONE;
        }
        foreach (JingleFileEncryptionHelper helper in JingleFileHelperRegistry.instance.encryption_helpers.values) {
            var encryption = helper.get_encryption(jingle_file_transfer);
            if (encryption != Encryption.NONE) return encryption;
        }
        return Encryption.NONE;
    }

    public async InputStream download(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta) throws FileReceiveError {
        // TODO(hrxi) What should happen if `stream == null`?
        XmppStream? stream = stream_interactor.get_stream(file_transfer.account);
        Xmpp.Xep.JingleFileTransfer.FileTransfer? jingle_file_transfer = file_transfers[file_transfer.info];
        if (jingle_file_transfer == null) {
            throw new FileReceiveError.DOWNLOAD_FAILED("Transfer data not available anymore");
        }
        try {
            yield jingle_file_transfer.accept(stream);
        } catch (IOError e) {
            throw new FileReceiveError.DOWNLOAD_FAILED("Establishing connection did not work");
        }
        return jingle_file_transfer.stream;
    }

    public int get_id() {
        return 1;
    }

    private void on_stream_negotiated(Account account, XmppStream stream) {
        stream_interactor.module_manager.get_module(account, Xmpp.Xep.JingleFileTransfer.Module.IDENTITY).file_incoming.connect((stream, jingle_file_transfer) => {
            Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(jingle_file_transfer.peer.bare_jid, account);
            if (conversation == null) {
                // TODO(hrxi): What to do?
                return;
            }
            string id = random_uuid();

            file_transfers[id] = jingle_file_transfer;

            FileMeta file_meta = new FileMeta();
            file_meta.size = jingle_file_transfer.size;
            file_meta.file_name = jingle_file_transfer.file_name;

            var time = new DateTime.now_utc();
            var from = jingle_file_transfer.peer.bare_jid;

            file_incoming(id, from, time, time, conversation, new FileReceiveData(), file_meta);
        });
    }
}

public class JingleFileSender : FileSender, Object {

    private StreamInteractor stream_interactor;

    public JingleFileSender(StreamInteractor stream_interactor) {
        this.stream_interactor = stream_interactor;
    }

    public async bool is_upload_available(Conversation conversation) {
        if (conversation.type_ != Conversation.Type.CHAT) return false;

        JingleFileEncryptionHelper? helper = JingleFileHelperRegistry.instance.get_encryption_helper(conversation.encryption);
        if (helper == null) return false;
        if (!helper.can_transfer(conversation)) return false;

        XmppStream? stream = stream_interactor.get_stream(conversation.account);
        if (stream == null) return false;

        Gee.List<Jid>? resources = stream.get_flag(Presence.Flag.IDENTITY).get_resources(conversation.counterpart);
        if (resources == null) return false;

        foreach (Jid full_jid in resources) {
            if (yield stream.get_module(Xep.JingleFileTransfer.Module.IDENTITY).is_available(stream, full_jid)) {
                return true;
            }
        }
        return false;
    }

    public async long get_file_size_limit(Conversation conversation) {
        if (yield can_send_conv(conversation)) {
            return int.MAX;
        }
        return -1;
    }

    public async bool can_send(Conversation conversation, FileTransfer file_transfer) {
        return yield can_send_conv(conversation);
    }

    private async bool can_send_conv(Conversation conversation) {
        if (conversation.type_ != Conversation.Type.CHAT) return false;

        // No file specific restrictions apply to Jingle file transfers
        return yield is_upload_available(conversation);
    }

    public async bool can_encrypt(Conversation conversation, FileTransfer file_transfer) {
        JingleFileEncryptionHelper? helper = JingleFileHelperRegistry.instance.get_encryption_helper(file_transfer.encryption);
        if (helper == null) return false;
        return yield helper.can_encrypt(conversation, file_transfer);
    }

    public async FileSendData? prepare_send_file(Conversation conversation, FileTransfer file_transfer, FileMeta file_meta) throws FileSendError {
        if (file_meta is HttpFileMeta) {
            throw new FileSendError.UPLOAD_FAILED("Cannot upload http file meta over Jingle");
        }
        return new FileSendData();
    }

    public async void send_file(Conversation conversation, FileTransfer file_transfer, FileSendData file_send_data, FileMeta file_meta) throws FileSendError {
        XmppStream? stream = stream_interactor.get_stream(file_transfer.account);
        if (stream == null) throw new FileSendError.UPLOAD_FAILED("No stream available");
        JingleFileEncryptionHelper? helper = JingleFileHelperRegistry.instance.get_encryption_helper(file_transfer.encryption);
        bool must_encrypt = helper != null && yield helper.can_encrypt(conversation, file_transfer);
        // TODO(hrxi): Prioritization of transports (and resources?).
        foreach (Jid full_jid in stream.get_flag(Presence.Flag.IDENTITY).get_resources(conversation.counterpart)) {
            if (full_jid.equals(stream.get_flag(Bind.Flag.IDENTITY).my_jid)) {
                continue;
            }
            if (!yield stream.get_module(Xep.JingleFileTransfer.Module.IDENTITY).is_available(stream, full_jid)) {
                continue;
            }
            if (must_encrypt && !yield helper.can_encrypt(conversation, file_transfer, full_jid)) {
                continue;
            }
            string? precondition_name = null;
            Object? precondition_options = null;
            if (must_encrypt) {
                precondition_name = helper.get_precondition_name(conversation, file_transfer);
                precondition_options = helper.get_precondition_options(conversation, file_transfer);
                if (precondition_name == null) {
                    throw new FileSendError.ENCRYPTION_FAILED("Should have created a precondition, but did not");
                }
            }
            try {
                yield stream.get_module(Xep.JingleFileTransfer.Module.IDENTITY).offer_file_stream(stream, full_jid, file_transfer.input_stream, file_transfer.server_file_name, file_meta.size, precondition_name, precondition_options);
            } catch (Error e) {
                throw new FileSendError.UPLOAD_FAILED(@"offer_file_stream failed: $(e.message)");
            }
            return;
        }
    }

    public int get_id() { return 1; }

    public float get_priority() { return 50; }
}

}