Line data Source code
1 : /* SPDX-License-Identifier: GPL-3.0-or-later */
2 : /* Copyright 2026 Peter Csaszar */
3 :
4 : /**
5 : * @file domain/write/upload.c
6 : * @brief upload.saveFilePart + messages.sendMedia (US-P6-02).
7 : */
8 :
9 : #include "domain/write/upload.h"
10 :
11 : #include "app/dc_session.h"
12 : #include "tl_serial.h"
13 : #include "tl_registry.h"
14 : #include "mtproto_rpc.h"
15 : #include "crypto.h"
16 : #include "logger.h"
17 : #include "raii.h"
18 :
19 : #include <stdio.h>
20 : #include <stdlib.h>
21 : #include <string.h>
22 : #include <sys/stat.h>
23 :
24 : #define CRC_upload_saveFilePart 0xb304a621u
25 : #define CRC_upload_saveBigFilePart 0xde7b673du
26 : #define CRC_messages_sendMedia 0x7547c966u
27 : #define CRC_inputFile 0xf52ff27fu
28 : #define CRC_inputFileBig 0xfa4f0bb5u
29 : #define CRC_inputMediaUploadedDocument 0x5b38c6c1u
30 : #define CRC_inputMediaUploadedPhoto 0x1e287d04u
31 : #define CRC_documentAttributeFilename 0x15590068u
32 :
33 9 : static int write_input_peer(TlWriter *w, const HistoryPeer *p) {
34 9 : switch (p->kind) {
35 9 : case HISTORY_PEER_SELF:
36 9 : tl_write_uint32(w, TL_inputPeerSelf); return 0;
37 0 : case HISTORY_PEER_USER:
38 0 : tl_write_uint32(w, TL_inputPeerUser);
39 0 : tl_write_int64 (w, p->peer_id);
40 0 : tl_write_int64 (w, p->access_hash); return 0;
41 0 : case HISTORY_PEER_CHAT:
42 0 : tl_write_uint32(w, TL_inputPeerChat);
43 0 : tl_write_int64 (w, p->peer_id); return 0;
44 0 : case HISTORY_PEER_CHANNEL:
45 0 : tl_write_uint32(w, TL_inputPeerChannel);
46 0 : tl_write_int64 (w, p->peer_id);
47 0 : tl_write_int64 (w, p->access_hash); return 0;
48 0 : default: return -1;
49 : }
50 : }
51 :
52 : /* Send one upload.saveFilePart (small) or upload.saveBigFilePart (big).
53 : * Returns 0 on boolTrue, -1 on error. When @p out_migrate_dc is non-NULL,
54 : * the server's migrate_dc hint (FILE_MIGRATE_X / NETWORK_MIGRATE_X) is
55 : * written there so the caller can switch DCs. */
56 32 : static int save_part(const ApiConfig *cfg,
57 : MtProtoSession *s, Transport *t,
58 : int is_big,
59 : int64_t file_id, int32_t part_idx, int32_t total_parts,
60 : const uint8_t *bytes, size_t len,
61 : int *out_migrate_dc) {
62 32 : if (out_migrate_dc) *out_migrate_dc = 0;
63 :
64 32 : TlWriter w; tl_writer_init(&w);
65 32 : if (is_big) {
66 21 : tl_write_uint32(&w, CRC_upload_saveBigFilePart);
67 21 : tl_write_int64 (&w, file_id);
68 21 : tl_write_int32 (&w, part_idx);
69 21 : tl_write_int32 (&w, total_parts);
70 21 : tl_write_bytes (&w, bytes, len);
71 : } else {
72 11 : tl_write_uint32(&w, CRC_upload_saveFilePart);
73 11 : tl_write_int64 (&w, file_id);
74 11 : tl_write_int32 (&w, part_idx);
75 11 : tl_write_bytes (&w, bytes, len);
76 : }
77 :
78 32 : RAII_STRING uint8_t *query = (uint8_t *)malloc(w.len);
79 32 : if (!query) { tl_writer_free(&w); return -1; }
80 32 : memcpy(query, w.data, w.len);
81 32 : size_t qlen = w.len;
82 32 : tl_writer_free(&w);
83 :
84 32 : uint8_t resp[256]; size_t resp_len = 0;
85 32 : if (api_call(cfg, s, t, query, qlen, resp, sizeof(resp), &resp_len) != 0)
86 0 : return -1;
87 32 : if (resp_len < 4) return -1;
88 32 : uint32_t top; memcpy(&top, resp, 4);
89 32 : if (top == TL_rpc_error) {
90 2 : RpcError perr; rpc_parse_error(resp, resp_len, &perr);
91 2 : if (out_migrate_dc && perr.migrate_dc > 0)
92 2 : *out_migrate_dc = perr.migrate_dc;
93 2 : logger_log(LOG_ERROR, "upload: save%sFilePart RPC %d: %s (migrate=%d)",
94 : is_big ? "Big" : "",
95 : perr.error_code, perr.error_msg, perr.migrate_dc);
96 2 : return -1;
97 : }
98 30 : if (top != TL_boolTrue) {
99 0 : logger_log(LOG_ERROR, "upload: unexpected save%sFilePart reply 0x%08x",
100 : is_big ? "Big" : "", top);
101 0 : return -1;
102 : }
103 30 : return 0;
104 : }
105 :
106 : /* Upload @p fp fully on the given (s, t). Caller must have rewound @p fp.
107 : * On NETWORK_MIGRATE_X / FILE_MIGRATE_X at any part, writes the target DC
108 : * to @p out_migrate_dc and returns -1 without continuing. */
109 11 : static int upload_all_parts(const ApiConfig *cfg,
110 : MtProtoSession *s, Transport *t,
111 : FILE *fp, int is_big,
112 : int64_t total,
113 : int64_t file_id, int32_t total_parts,
114 : uint8_t *chunk,
115 : int32_t *out_part_count,
116 : int *out_migrate_dc) {
117 11 : if (out_migrate_dc) *out_migrate_dc = 0;
118 11 : if (fseek(fp, 0, SEEK_SET) != 0) return -1;
119 :
120 11 : int32_t part_idx = 0;
121 11 : int64_t done = 0;
122 41 : while (done < total) {
123 32 : int64_t want = total - done;
124 32 : if (want > UPLOAD_CHUNK_SIZE) want = UPLOAD_CHUNK_SIZE;
125 32 : size_t got = fread(chunk, 1, (size_t)want, fp);
126 32 : if ((int64_t)got != want) {
127 0 : logger_log(LOG_ERROR, "upload: short read at part %d", part_idx);
128 2 : return -1;
129 : }
130 32 : int migrate = 0;
131 32 : if (save_part(cfg, s, t, is_big, file_id, part_idx, total_parts,
132 : chunk, got, &migrate) != 0) {
133 2 : if (out_migrate_dc && migrate > 0) *out_migrate_dc = migrate;
134 2 : logger_log(LOG_ERROR, "upload: part %d failed (migrate=%d)",
135 : part_idx, migrate);
136 2 : return -1;
137 : }
138 30 : done += (int64_t)got;
139 30 : part_idx++;
140 : }
141 9 : if (out_part_count) *out_part_count = part_idx;
142 9 : return 0;
143 : }
144 :
145 : /* Extract the basename of a path, e.g. "/a/b/c.jpg" → "c.jpg". */
146 9 : static const char *basename_of(const char *path) {
147 9 : const char *slash = strrchr(path, '/');
148 9 : return slash ? slash + 1 : path;
149 : }
150 :
151 6 : int domain_path_is_image(const char *path) {
152 6 : if (!path) return 0;
153 5 : const char *dot = strrchr(path, '.');
154 5 : if (!dot) return 0;
155 4 : const char *ext = dot + 1;
156 : /* Case-insensitive extension match. */
157 4 : char buf[8] = {0};
158 4 : size_t n = strlen(ext);
159 4 : if (n == 0 || n >= sizeof(buf)) return 0;
160 17 : for (size_t i = 0; i < n; i++) {
161 13 : unsigned c = (unsigned char)ext[i];
162 13 : if (c >= 'A' && c <= 'Z') c += 32;
163 13 : buf[i] = (char)c;
164 : }
165 7 : return (strcmp(buf, "jpg") == 0 ||
166 3 : strcmp(buf, "jpeg") == 0 ||
167 3 : strcmp(buf, "png") == 0 ||
168 2 : strcmp(buf, "webp") == 0 ||
169 7 : strcmp(buf, "gif") == 0) ? 1 : 0;
170 : }
171 :
172 : /* Chunked upload step: open @p file_path, generate a random file_id,
173 : * push every part via upload_all_parts (with cross-DC migration), and
174 : * hand the resulting (file_id, part_count, is_big) back to the caller
175 : * so it can compose the second-phase messages.sendMedia. */
176 : typedef struct {
177 : int64_t file_id;
178 : int32_t part_count;
179 : int is_big;
180 : char file_name[256];
181 : } UploadedFile;
182 :
183 12 : static int upload_chunk_phase(const ApiConfig *cfg,
184 : MtProtoSession *s, Transport *t,
185 : const char *file_path,
186 : UploadedFile *uf) {
187 : struct stat st;
188 12 : if (stat(file_path, &st) != 0 || !S_ISREG(st.st_mode)) {
189 1 : logger_log(LOG_ERROR, "upload: cannot stat %s", file_path);
190 1 : return -1;
191 : }
192 11 : if (st.st_size <= 0 || (int64_t)st.st_size > UPLOAD_MAX_SIZE) {
193 2 : logger_log(LOG_ERROR,
194 : "upload: size %lld out of supported range (1..%lld)",
195 2 : (long long)st.st_size, (long long)UPLOAD_MAX_SIZE);
196 2 : return -1;
197 : }
198 9 : int is_big = ((int64_t)st.st_size >= UPLOAD_BIG_THRESHOLD);
199 9 : int64_t total = (int64_t)st.st_size;
200 9 : int32_t total_parts = (int32_t)((total + UPLOAD_CHUNK_SIZE - 1)
201 9 : / UPLOAD_CHUNK_SIZE);
202 :
203 18 : RAII_FILE FILE *fp = fopen(file_path, "rb");
204 9 : if (!fp) {
205 0 : logger_log(LOG_ERROR, "upload: cannot open %s", file_path);
206 0 : return -1;
207 : }
208 :
209 9 : uint8_t id_buf[8] = {0};
210 9 : int64_t file_id = 0;
211 9 : if (crypto_rand_bytes(id_buf, 8) != 0) return -1;
212 9 : memcpy(&file_id, id_buf, 8);
213 :
214 9 : RAII_STRING uint8_t *chunk = (uint8_t *)malloc(UPLOAD_CHUNK_SIZE);
215 9 : if (!chunk) return -1;
216 :
217 9 : int32_t part_idx = 0;
218 9 : int home_migrate = 0;
219 9 : if (upload_all_parts(cfg, s, t, fp, is_big, total,
220 : file_id, total_parts, chunk,
221 : &part_idx, &home_migrate) != 0) {
222 2 : if (home_migrate <= 0) {
223 0 : logger_log(LOG_ERROR, "upload: non-migrate failure");
224 0 : return -1;
225 : }
226 2 : logger_log(LOG_INFO,
227 : "upload: NETWORK/FILE_MIGRATE_%d, retrying on DC%d",
228 : home_migrate, home_migrate);
229 :
230 : DcSession xdc;
231 2 : if (dc_session_open(home_migrate, &xdc) != 0) return -1;
232 2 : if (dc_session_ensure_authorized(&xdc, cfg, s, t) != 0) {
233 0 : dc_session_close(&xdc);
234 0 : return -1;
235 : }
236 :
237 2 : uint8_t id2_buf[8] = {0};
238 2 : if (crypto_rand_bytes(id2_buf, 8) != 0) {
239 0 : dc_session_close(&xdc); return -1;
240 : }
241 2 : memcpy(&file_id, id2_buf, 8);
242 :
243 2 : int fdc_migrate = 0;
244 2 : int rc2 = upload_all_parts(cfg, &xdc.session, &xdc.transport, fp,
245 : is_big, total, file_id, total_parts,
246 : chunk, &part_idx, &fdc_migrate);
247 2 : dc_session_close(&xdc);
248 2 : if (rc2 != 0) {
249 0 : logger_log(LOG_ERROR, "upload: retry on DC%d also failed",
250 : home_migrate);
251 0 : return -1;
252 : }
253 : }
254 :
255 9 : uf->file_id = file_id;
256 9 : uf->part_count = part_idx;
257 9 : uf->is_big = is_big;
258 9 : const char *bn = basename_of(file_path);
259 9 : size_t bn_n = strlen(bn);
260 9 : if (bn_n >= sizeof(uf->file_name)) bn_n = sizeof(uf->file_name) - 1;
261 9 : memcpy(uf->file_name, bn, bn_n);
262 9 : uf->file_name[bn_n] = '\0';
263 9 : return 0;
264 : }
265 :
266 : /* Serialise an InputFile / InputFileBig reference body into @p w. */
267 9 : static void write_input_file(TlWriter *w, const UploadedFile *uf) {
268 9 : if (uf->is_big) {
269 1 : tl_write_uint32(w, CRC_inputFileBig);
270 1 : tl_write_int64 (w, uf->file_id);
271 1 : tl_write_int32 (w, uf->part_count);
272 1 : tl_write_string(w, uf->file_name);
273 : } else {
274 8 : tl_write_uint32(w, CRC_inputFile);
275 8 : tl_write_int64 (w, uf->file_id);
276 8 : tl_write_int32 (w, uf->part_count);
277 8 : tl_write_string(w, uf->file_name);
278 8 : tl_write_string(w, ""); /* md5_checksum */
279 : }
280 9 : }
281 :
282 : /* Dispatch messages.sendMedia and drain the response. */
283 9 : static int send_media_call(const ApiConfig *cfg,
284 : MtProtoSession *s, Transport *t,
285 : const uint8_t *query, size_t qlen,
286 : RpcError *err) {
287 9 : uint8_t resp[4096]; size_t resp_len = 0;
288 9 : if (api_call(cfg, s, t, query, qlen, resp, sizeof(resp), &resp_len) != 0) {
289 0 : logger_log(LOG_ERROR, "upload: sendMedia api_call failed");
290 0 : return -1;
291 : }
292 9 : if (resp_len < 4) return -1;
293 9 : uint32_t top; memcpy(&top, resp, 4);
294 9 : if (top == TL_rpc_error) {
295 0 : if (err) rpc_parse_error(resp, resp_len, err);
296 0 : return -1;
297 : }
298 9 : if (top == TL_updates || top == TL_updatesCombined
299 0 : || top == TL_updateShort) {
300 9 : return 0;
301 : }
302 0 : logger_log(LOG_WARN, "upload: unexpected sendMedia top 0x%08x", top);
303 0 : return 0;
304 : }
305 :
306 12 : int domain_send_file(const ApiConfig *cfg,
307 : MtProtoSession *s, Transport *t,
308 : const HistoryPeer *peer,
309 : const char *file_path,
310 : const char *caption,
311 : const char *mime_type,
312 : RpcError *err) {
313 12 : if (!cfg || !s || !t || !peer || !file_path) return -1;
314 11 : if (!mime_type) mime_type = "application/octet-stream";
315 11 : if (!caption) caption = "";
316 :
317 11 : UploadedFile uf = {0};
318 11 : if (upload_chunk_phase(cfg, s, t, file_path, &uf) != 0) return -1;
319 :
320 8 : uint8_t rand_rnd[8] = {0};
321 8 : int64_t random_id = 0;
322 8 : if (crypto_rand_bytes(rand_rnd, 8) == 0) memcpy(&random_id, rand_rnd, 8);
323 :
324 8 : TlWriter w; tl_writer_init(&w);
325 8 : tl_write_uint32(&w, CRC_messages_sendMedia);
326 8 : tl_write_uint32(&w, 0); /* flags = 0 */
327 8 : if (write_input_peer(&w, peer) != 0) {
328 0 : tl_writer_free(&w);
329 0 : return -1;
330 : }
331 : /* media: InputMediaUploadedDocument */
332 8 : tl_write_uint32(&w, CRC_inputMediaUploadedDocument);
333 8 : tl_write_uint32(&w, 0); /* inner flags */
334 8 : write_input_file(&w, &uf);
335 8 : tl_write_string(&w, mime_type);
336 : /* attributes: Vector<DocumentAttribute> with a single filename. */
337 8 : tl_write_uint32(&w, TL_vector);
338 8 : tl_write_uint32(&w, 1);
339 8 : tl_write_uint32(&w, CRC_documentAttributeFilename);
340 8 : tl_write_string(&w, uf.file_name);
341 :
342 8 : tl_write_string(&w, caption); /* message */
343 8 : tl_write_int64 (&w, random_id);
344 :
345 8 : RAII_STRING uint8_t *query = (uint8_t *)malloc(w.len);
346 8 : if (!query) { tl_writer_free(&w); return -1; }
347 8 : memcpy(query, w.data, w.len);
348 8 : size_t qlen = w.len;
349 8 : tl_writer_free(&w);
350 :
351 8 : return send_media_call(cfg, s, t, query, qlen, err);
352 : }
353 :
354 1 : int domain_send_photo(const ApiConfig *cfg,
355 : MtProtoSession *s, Transport *t,
356 : const HistoryPeer *peer,
357 : const char *file_path,
358 : const char *caption,
359 : RpcError *err) {
360 1 : if (!cfg || !s || !t || !peer || !file_path) return -1;
361 1 : if (!caption) caption = "";
362 :
363 1 : UploadedFile uf = {0};
364 1 : if (upload_chunk_phase(cfg, s, t, file_path, &uf) != 0) return -1;
365 :
366 1 : uint8_t rand_rnd[8] = {0};
367 1 : int64_t random_id = 0;
368 1 : if (crypto_rand_bytes(rand_rnd, 8) == 0) memcpy(&random_id, rand_rnd, 8);
369 :
370 1 : TlWriter w; tl_writer_init(&w);
371 1 : tl_write_uint32(&w, CRC_messages_sendMedia);
372 1 : tl_write_uint32(&w, 0); /* outer flags = 0 */
373 1 : if (write_input_peer(&w, peer) != 0) {
374 0 : tl_writer_free(&w);
375 0 : return -1;
376 : }
377 : /* media: InputMediaUploadedPhoto#1e287d04 flags:# spoiler:flags.2?true
378 : * file:InputFile stickers:flags.0?Vector<InputDocument>
379 : * ttl_seconds:flags.1?int
380 : * All optional flags are clear here; server rescales. */
381 1 : tl_write_uint32(&w, CRC_inputMediaUploadedPhoto);
382 1 : tl_write_uint32(&w, 0); /* inner flags */
383 1 : write_input_file(&w, &uf);
384 :
385 1 : tl_write_string(&w, caption);
386 1 : tl_write_int64 (&w, random_id);
387 :
388 1 : RAII_STRING uint8_t *query = (uint8_t *)malloc(w.len);
389 1 : if (!query) { tl_writer_free(&w); return -1; }
390 1 : memcpy(query, w.data, w.len);
391 1 : size_t qlen = w.len;
392 1 : tl_writer_free(&w);
393 :
394 1 : return send_media_call(cfg, s, t, query, qlen, err);
395 : }
|