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 22 : static int write_input_peer(TlWriter *w, const HistoryPeer *p) {
34 22 : switch (p->kind) {
35 22 : case HISTORY_PEER_SELF:
36 22 : 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 92 : 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 92 : if (out_migrate_dc) *out_migrate_dc = 0;
63 :
64 92 : TlWriter w; tl_writer_init(&w);
65 92 : if (is_big) {
66 66 : tl_write_uint32(&w, CRC_upload_saveBigFilePart);
67 66 : tl_write_int64 (&w, file_id);
68 66 : tl_write_int32 (&w, part_idx);
69 66 : tl_write_int32 (&w, total_parts);
70 66 : tl_write_bytes (&w, bytes, len);
71 : } else {
72 26 : tl_write_uint32(&w, CRC_upload_saveFilePart);
73 26 : tl_write_int64 (&w, file_id);
74 26 : tl_write_int32 (&w, part_idx);
75 26 : tl_write_bytes (&w, bytes, len);
76 : }
77 :
78 92 : RAII_STRING uint8_t *query = (uint8_t *)malloc(w.len);
79 92 : if (!query) { tl_writer_free(&w); return -1; }
80 92 : memcpy(query, w.data, w.len);
81 92 : size_t qlen = w.len;
82 92 : tl_writer_free(&w);
83 :
84 92 : uint8_t resp[256]; size_t resp_len = 0;
85 92 : if (api_call(cfg, s, t, query, qlen, resp, sizeof(resp), &resp_len) != 0)
86 0 : return -1;
87 92 : if (resp_len < 4) return -1;
88 92 : uint32_t top; memcpy(&top, resp, 4);
89 92 : if (top == TL_rpc_error) {
90 5 : RpcError perr; rpc_parse_error(resp, resp_len, &perr);
91 5 : if (out_migrate_dc && perr.migrate_dc > 0)
92 4 : *out_migrate_dc = perr.migrate_dc;
93 5 : 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 5 : return -1;
97 : }
98 87 : 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 87 : 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 27 : 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 27 : if (out_migrate_dc) *out_migrate_dc = 0;
118 27 : if (fseek(fp, 0, SEEK_SET) != 0) return -1;
119 :
120 27 : int32_t part_idx = 0;
121 27 : int64_t done = 0;
122 114 : while (done < total) {
123 92 : int64_t want = total - done;
124 92 : if (want > UPLOAD_CHUNK_SIZE) want = UPLOAD_CHUNK_SIZE;
125 92 : size_t got = fread(chunk, 1, (size_t)want, fp);
126 92 : if ((int64_t)got != want) {
127 0 : logger_log(LOG_ERROR, "upload: short read at part %d", part_idx);
128 5 : return -1;
129 : }
130 92 : int migrate = 0;
131 92 : if (save_part(cfg, s, t, is_big, file_id, part_idx, total_parts,
132 : chunk, got, &migrate) != 0) {
133 5 : if (out_migrate_dc && migrate > 0) *out_migrate_dc = migrate;
134 5 : logger_log(LOG_ERROR, "upload: part %d failed (migrate=%d)",
135 : part_idx, migrate);
136 5 : return -1;
137 : }
138 87 : done += (int64_t)got;
139 87 : part_idx++;
140 : }
141 22 : if (out_part_count) *out_part_count = part_idx;
142 22 : return 0;
143 : }
144 :
145 : /* Extract the basename of a path, e.g. "/a/b/c.jpg" → "c.jpg". */
146 22 : static const char *basename_of(const char *path) {
147 22 : const char *slash = strrchr(path, '/');
148 22 : return slash ? slash + 1 : path;
149 : }
150 :
151 22 : int domain_path_is_image(const char *path) {
152 22 : if (!path) return 0;
153 19 : const char *dot = strrchr(path, '.');
154 19 : if (!dot) return 0;
155 15 : const char *ext = dot + 1;
156 : /* Case-insensitive extension match. */
157 15 : char buf[8] = {0};
158 15 : size_t n = strlen(ext);
159 15 : if (n == 0 || n >= sizeof(buf)) return 0;
160 64 : for (size_t i = 0; i < n; i++) {
161 49 : unsigned c = (unsigned char)ext[i];
162 49 : if (c >= 'A' && c <= 'Z') c += 32;
163 49 : buf[i] = (char)c;
164 : }
165 27 : return (strcmp(buf, "jpg") == 0 ||
166 12 : strcmp(buf, "jpeg") == 0 ||
167 11 : strcmp(buf, "png") == 0 ||
168 8 : strcmp(buf, "webp") == 0 ||
169 27 : 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 33 : 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 33 : if (stat(file_path, &st) != 0 || !S_ISREG(st.st_mode)) {
189 4 : logger_log(LOG_ERROR, "upload: cannot stat %s", file_path);
190 4 : return -1;
191 : }
192 29 : if (st.st_size <= 0 || (int64_t)st.st_size > UPLOAD_MAX_SIZE) {
193 6 : logger_log(LOG_ERROR,
194 : "upload: size %lld out of supported range (1..%lld)",
195 6 : (long long)st.st_size, (long long)UPLOAD_MAX_SIZE);
196 6 : return -1;
197 : }
198 23 : int is_big = ((int64_t)st.st_size >= UPLOAD_BIG_THRESHOLD);
199 23 : int64_t total = (int64_t)st.st_size;
200 23 : int32_t total_parts = (int32_t)((total + UPLOAD_CHUNK_SIZE - 1)
201 23 : / UPLOAD_CHUNK_SIZE);
202 :
203 46 : RAII_FILE FILE *fp = fopen(file_path, "rb");
204 23 : if (!fp) {
205 0 : logger_log(LOG_ERROR, "upload: cannot open %s", file_path);
206 0 : return -1;
207 : }
208 :
209 23 : uint8_t id_buf[8] = {0};
210 23 : int64_t file_id = 0;
211 23 : if (crypto_rand_bytes(id_buf, 8) != 0) return -1;
212 23 : memcpy(&file_id, id_buf, 8);
213 :
214 23 : RAII_STRING uint8_t *chunk = (uint8_t *)malloc(UPLOAD_CHUNK_SIZE);
215 23 : if (!chunk) return -1;
216 :
217 23 : int32_t part_idx = 0;
218 23 : int home_migrate = 0;
219 23 : if (upload_all_parts(cfg, s, t, fp, is_big, total,
220 : file_id, total_parts, chunk,
221 : &part_idx, &home_migrate) != 0) {
222 5 : if (home_migrate <= 0) {
223 1 : logger_log(LOG_ERROR, "upload: non-migrate failure");
224 1 : return -1;
225 : }
226 4 : logger_log(LOG_INFO,
227 : "upload: NETWORK/FILE_MIGRATE_%d, retrying on DC%d",
228 : home_migrate, home_migrate);
229 :
230 : DcSession xdc;
231 4 : if (dc_session_open(home_migrate, &xdc) != 0) return -1;
232 4 : if (dc_session_ensure_authorized(&xdc, cfg, s, t) != 0) {
233 0 : dc_session_close(&xdc);
234 0 : return -1;
235 : }
236 :
237 4 : uint8_t id2_buf[8] = {0};
238 4 : if (crypto_rand_bytes(id2_buf, 8) != 0) {
239 0 : dc_session_close(&xdc); return -1;
240 : }
241 4 : memcpy(&file_id, id2_buf, 8);
242 :
243 4 : int fdc_migrate = 0;
244 4 : 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 4 : dc_session_close(&xdc);
248 4 : 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 22 : uf->file_id = file_id;
256 22 : uf->part_count = part_idx;
257 22 : uf->is_big = is_big;
258 22 : const char *bn = basename_of(file_path);
259 22 : size_t bn_n = strlen(bn);
260 22 : if (bn_n >= sizeof(uf->file_name)) bn_n = sizeof(uf->file_name) - 1;
261 22 : memcpy(uf->file_name, bn, bn_n);
262 22 : uf->file_name[bn_n] = '\0';
263 22 : return 0;
264 : }
265 :
266 : /* Serialise an InputFile / InputFileBig reference body into @p w. */
267 22 : static void write_input_file(TlWriter *w, const UploadedFile *uf) {
268 22 : if (uf->is_big) {
269 3 : tl_write_uint32(w, CRC_inputFileBig);
270 3 : tl_write_int64 (w, uf->file_id);
271 3 : tl_write_int32 (w, uf->part_count);
272 3 : tl_write_string(w, uf->file_name);
273 : } else {
274 19 : tl_write_uint32(w, CRC_inputFile);
275 19 : tl_write_int64 (w, uf->file_id);
276 19 : tl_write_int32 (w, uf->part_count);
277 19 : tl_write_string(w, uf->file_name);
278 19 : tl_write_string(w, ""); /* md5_checksum */
279 : }
280 22 : }
281 :
282 : /* Dispatch messages.sendMedia and drain the response. */
283 22 : 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 22 : uint8_t resp[4096]; size_t resp_len = 0;
288 22 : 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 22 : if (resp_len < 4) return -1;
293 22 : uint32_t top; memcpy(&top, resp, 4);
294 22 : if (top == TL_rpc_error) {
295 0 : if (err) rpc_parse_error(resp, resp_len, err);
296 0 : return -1;
297 : }
298 22 : if (top == TL_updates || top == TL_updatesCombined
299 4 : || top == TL_updateShort) {
300 22 : return 0;
301 : }
302 0 : logger_log(LOG_WARN, "upload: unexpected sendMedia top 0x%08x", top);
303 0 : return 0;
304 : }
305 :
306 32 : 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 32 : if (!cfg || !s || !t || !peer || !file_path) return -1;
314 29 : if (!mime_type) mime_type = "application/octet-stream";
315 29 : if (!caption) caption = "";
316 :
317 29 : UploadedFile uf = {0};
318 29 : if (upload_chunk_phase(cfg, s, t, file_path, &uf) != 0) return -1;
319 :
320 19 : uint8_t rand_rnd[8] = {0};
321 19 : int64_t random_id = 0;
322 19 : if (crypto_rand_bytes(rand_rnd, 8) == 0) memcpy(&random_id, rand_rnd, 8);
323 :
324 19 : TlWriter w; tl_writer_init(&w);
325 19 : tl_write_uint32(&w, CRC_messages_sendMedia);
326 19 : tl_write_uint32(&w, 0); /* flags = 0 */
327 19 : if (write_input_peer(&w, peer) != 0) {
328 0 : tl_writer_free(&w);
329 0 : return -1;
330 : }
331 : /* media: InputMediaUploadedDocument */
332 19 : tl_write_uint32(&w, CRC_inputMediaUploadedDocument);
333 19 : tl_write_uint32(&w, 0); /* inner flags */
334 19 : write_input_file(&w, &uf);
335 19 : tl_write_string(&w, mime_type);
336 : /* attributes: Vector<DocumentAttribute> with a single filename. */
337 19 : tl_write_uint32(&w, TL_vector);
338 19 : tl_write_uint32(&w, 1);
339 19 : tl_write_uint32(&w, CRC_documentAttributeFilename);
340 19 : tl_write_string(&w, uf.file_name);
341 :
342 19 : tl_write_string(&w, caption); /* message */
343 19 : tl_write_int64 (&w, random_id);
344 :
345 19 : RAII_STRING uint8_t *query = (uint8_t *)malloc(w.len);
346 19 : if (!query) { tl_writer_free(&w); return -1; }
347 19 : memcpy(query, w.data, w.len);
348 19 : size_t qlen = w.len;
349 19 : tl_writer_free(&w);
350 :
351 19 : return send_media_call(cfg, s, t, query, qlen, err);
352 : }
353 :
354 5 : 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 5 : if (!cfg || !s || !t || !peer || !file_path) return -1;
361 4 : if (!caption) caption = "";
362 :
363 4 : UploadedFile uf = {0};
364 4 : if (upload_chunk_phase(cfg, s, t, file_path, &uf) != 0) return -1;
365 :
366 3 : uint8_t rand_rnd[8] = {0};
367 3 : int64_t random_id = 0;
368 3 : if (crypto_rand_bytes(rand_rnd, 8) == 0) memcpy(&random_id, rand_rnd, 8);
369 :
370 3 : TlWriter w; tl_writer_init(&w);
371 3 : tl_write_uint32(&w, CRC_messages_sendMedia);
372 3 : tl_write_uint32(&w, 0); /* outer flags = 0 */
373 3 : 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 3 : tl_write_uint32(&w, CRC_inputMediaUploadedPhoto);
382 3 : tl_write_uint32(&w, 0); /* inner flags */
383 3 : write_input_file(&w, &uf);
384 :
385 3 : tl_write_string(&w, caption);
386 3 : tl_write_int64 (&w, random_id);
387 :
388 3 : RAII_STRING uint8_t *query = (uint8_t *)malloc(w.len);
389 3 : if (!query) { tl_writer_free(&w); return -1; }
390 3 : memcpy(query, w.data, w.len);
391 3 : size_t qlen = w.len;
392 3 : tl_writer_free(&w);
393 :
394 3 : return send_media_call(cfg, s, t, query, qlen, err);
395 : }
|