rust_rcs_client/messaging/ft_http/
upload.rs

1// Copyright 2023 宋昊文
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::{
16    fmt::Display,
17    fs::File,
18    io::{copy, Seek},
19    path::MAIN_SEPARATOR,
20    sync::Arc,
21};
22
23use futures::{future::BoxFuture, AsyncReadExt, FutureExt};
24use rust_rcs_core::{
25    ffi::log::platform_log,
26    http::{
27        request::{Request, GET, POST, PUT},
28        HttpClient, HttpConnectionHandle,
29    },
30    internet::{
31        body::{
32            message_body::MessageBody,
33            multipart_body::MultipartBody,
34            streamed_body::{StreamSource, StreamedBody},
35        },
36        header, Body, Header,
37    },
38    io::Serializable,
39    security::{
40        authentication::digest::DigestAnswerParams,
41        gba::{self, GbaContext},
42        SecurityContext,
43    },
44    util::rand::create_raw_alpha_numeric_string,
45};
46use url::Url;
47use uuid::Uuid;
48
49use super::{
50    resume_info_xml::{parse_xml, ResumeInfo},
51    FileTransferOverHTTPService,
52};
53
54const LOG_TAG: &str = "fthttp";
55
56pub enum FileUploadError {
57    Http(u16, String),
58    IO,
59    MalformedHost,
60    NetworkIO,
61}
62
63impl FileUploadError {
64    pub fn error_code(&self) -> u16 {
65        match &self {
66            FileUploadError::Http(status_code, _) => *status_code,
67            FileUploadError::IO => 0,
68            FileUploadError::MalformedHost => 0,
69            FileUploadError::NetworkIO => 0,
70        }
71    }
72
73    pub fn error_string(&self) -> String {
74        match &self {
75            FileUploadError::Http(_, reason_phrase) => String::from(reason_phrase),
76            FileUploadError::IO => String::from("IO"),
77            FileUploadError::MalformedHost => String::from("MalformedHost"),
78            FileUploadError::NetworkIO => String::from("NetworkIO"),
79        }
80    }
81}
82
83impl Display for FileUploadError {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        match &self {
86            FileUploadError::Http(status_code, reason_phrase) => {
87                f.write_fmt(format_args!("Http {} {}", status_code, reason_phrase))
88            }
89            FileUploadError::IO => f.write_str("IO"),
90            FileUploadError::MalformedHost => f.write_str("MalformedHost"),
91            FileUploadError::NetworkIO => f.write_str("NetworkIO"),
92        }
93    }
94}
95
96async fn get_download_info_inner(
97    ft_http_cs_uri: &str,
98    tid: Uuid,
99    msisdn: Option<&str>,
100    http_client: &Arc<HttpClient>,
101    gba_context: &Arc<GbaContext>,
102    security_context: &Arc<SecurityContext>,
103    digest_answer: Option<&DigestAnswerParams>,
104) -> Result<String, FileUploadError> {
105    let url_string = format!(
106        "{}?tid={}&get_download_info",
107        ft_http_cs_uri,
108        tid.as_hyphenated().encode_lower(&mut Uuid::encode_buffer())
109    );
110
111    if let Ok(url) = Url::parse(&url_string) {
112        if let Ok(conn) = http_client.connect(&url, false).await {
113            let host = url.host_str().unwrap();
114
115            let mut req = Request::new_with_default_headers(GET, host, url.path(), url.query());
116
117            if let Some(msisdn) = msisdn {
118                req.headers.push(Header::new(
119                    b"X-3GPP-Intended-Identity",
120                    format!("tel:{}", msisdn),
121                ));
122            }
123
124            let preloaded_answer = match digest_answer {
125                Some(_) => None,
126                None => {
127                    platform_log(LOG_TAG, "using stored authorization info");
128                    security_context.preload_auth(gba_context, host, conn.cipher_id(), PUT, None)
129                }
130            };
131
132            let digest_answer = match digest_answer {
133                Some(digest_answer) => Some(digest_answer),
134                None => match &preloaded_answer {
135                    Some(preloaded_answer) => {
136                        platform_log(LOG_TAG, "using preloaded digest answer");
137                        Some(preloaded_answer)
138                    }
139                    None => None,
140                },
141            };
142
143            if let Some(digest_answer) = digest_answer {
144                if let Ok(authorization) = digest_answer.make_authorization_header(
145                    match &digest_answer.challenge {
146                        Some(challenge) => Some(&challenge.algorithm),
147                        None => None,
148                    },
149                    false,
150                    false,
151                ) {
152                    if let Ok(authorization) = String::from_utf8(authorization) {
153                        req.headers
154                            .push(Header::new(b"Authorization", String::from(authorization)));
155                    }
156                }
157            }
158
159            if let Ok((resp, resp_stream)) = conn.send(req, |_| {}).await {
160                if resp.status_code == 200 {
161                    if let Some(mut resp_stream) = resp_stream {
162                        let mut resp_data = Vec::new();
163                        if let Ok(size) = resp_stream.read_to_end(&mut resp_data).await {
164                            platform_log(
165                                LOG_TAG,
166                                format!("get_download_info_inner resp.data read {} bytes", &size),
167                            );
168                        }
169
170                        if let Ok(resp_string) = String::from_utf8(resp_data) {
171                            platform_log(
172                                LOG_TAG,
173                                format!("get_download_info_inner resp.string = {}", &resp_string),
174                            );
175
176                            return Ok(resp_string);
177                        }
178                    }
179                } else {
180                    return Err(FileUploadError::Http(
181                        resp.status_code,
182                        match String::from_utf8(resp.reason_phrase) {
183                            Ok(reason_phrase) => reason_phrase,
184                            Err(_) => String::from(""),
185                        },
186                    ));
187                }
188            }
189        }
190
191        Err(FileUploadError::NetworkIO)
192    } else {
193        Err(FileUploadError::MalformedHost)
194    }
195}
196
197fn get_download_info<'a, 'b: 'a>(
198    ft_http_cs_uri: &'b str,
199    tid: Uuid,
200    msisdn: Option<&'b str>,
201    http_client: &'b Arc<HttpClient>,
202    gba_context: &'b Arc<GbaContext>,
203    security_context: &'b Arc<SecurityContext>,
204    digest_answer: Option<&'a DigestAnswerParams>,
205) -> BoxFuture<'a, Result<String, FileUploadError>> {
206    async move {
207        get_download_info_inner(
208            ft_http_cs_uri,
209            tid,
210            msisdn,
211            http_client,
212            gba_context,
213            security_context,
214            digest_answer,
215        )
216        .await
217    }
218    .boxed()
219}
220
221async fn resume_upload_put_inner(
222    ft_http_cs_uri: &str,
223    tid: Uuid,
224    file: FileInfo<'_>,
225    resume_info: &ResumeInfo,
226    msisdn: Option<&str>,
227    http_client: &Arc<HttpClient>,
228    gba_context: &Arc<GbaContext>,
229    security_context: &Arc<SecurityContext>,
230    digest_answer: Option<&DigestAnswerParams>,
231    progress_callback: &Arc<Box<dyn Fn(u32, i32) + Send + Sync>>,
232) -> Result<String, FileUploadError> {
233    if let Ok(url) = Url::parse(&resume_info.data_url) {
234        if let Ok(conn) = http_client.connect(&url, false).await {
235            let host = url.host_str().unwrap();
236
237            let mut req = Request::new_with_default_headers(PUT, host, url.path(), url.query());
238
239            if let Some(msisdn) = msisdn {
240                req.headers.push(Header::new(
241                    b"X-3GPP-Intended-Identity",
242                    format!("tel:{}", msisdn),
243                ));
244            }
245
246            let preloaded_answer = match digest_answer {
247                Some(_) => None,
248                None => {
249                    platform_log(LOG_TAG, "using stored authorization info");
250                    security_context.preload_auth(gba_context, host, conn.cipher_id(), PUT, None)
251                    // fix-me: we do have to perform digest here
252                }
253            };
254
255            let digest_answer = match digest_answer {
256                Some(digest_answer) => Some(digest_answer),
257                None => match &preloaded_answer {
258                    Some(preloaded_answer) => {
259                        platform_log(LOG_TAG, "using preloaded digest answer");
260                        Some(preloaded_answer)
261                    }
262                    None => None,
263                },
264            };
265
266            if let Some(digest_answer) = digest_answer {
267                if let Ok(authorization) = digest_answer.make_authorization_header(
268                    match &digest_answer.challenge {
269                        Some(challenge) => Some(&challenge.algorithm),
270                        None => None,
271                    },
272                    false,
273                    false,
274                ) {
275                    if let Ok(authorization) = String::from_utf8(authorization) {
276                        req.headers
277                            .push(Header::new(b"Authorization", String::from(authorization)));
278                    }
279                }
280            }
281
282            let mut file_size = 0;
283            if let Ok(mut f) = File::open(file.path) {
284                if let Ok(meta) = f.metadata() {
285                    file_size = meta.len();
286                } else {
287                    if let Ok(size) = f.seek(std::io::SeekFrom::End(0)) {
288                        file_size = size;
289                    }
290                }
291            }
292
293            let file_size = match usize::try_from(file_size) {
294                Ok(file_size) => {
295                    if file_size > 0 {
296                        file_size
297                    } else {
298                        return Err(FileUploadError::IO);
299                    }
300                }
301                Err(_) => return Err(FileUploadError::IO),
302            };
303
304            if resume_info.range_end + 1 >= file_size {
305                return Err(FileUploadError::IO);
306            }
307
308            let http_body = Body::Streamed(StreamedBody {
309                stream_size: file_size - resume_info.range_end,
310                stream_source: StreamSource::File((String::from(file.path), resume_info.range_end)),
311            });
312
313            req.headers
314                .push(Header::new("Content-Type", "application/octet-stream"));
315
316            req.headers.push(Header::new(
317                "Content-Range",
318                format!("{}-{}/{}", resume_info.range_end, file_size - 1, file_size),
319            ));
320
321            let progress_total = http_body.estimated_size();
322
323            req.headers
324                .push(Header::new("Content-Length", format!("{}", progress_total)));
325
326            req.body = Some(http_body);
327
328            let progress_callback_ = Arc::clone(progress_callback);
329
330            if let Ok((resp, _)) = conn
331                .send(req, move |written| {
332                    if let Ok(current) = u32::try_from(written) {
333                        if let Ok(total) = i32::try_from(progress_total) {
334                            progress_callback_(current, total);
335                        } else {
336                            progress_callback_(current, -1);
337                        }
338                    }
339                })
340                .await
341            {
342                if resp.status_code == 200 {
343                    if let Some(authentication_info_header) =
344                        header::search(&resp.headers, b"Authentication-Info", false)
345                    {
346                        if let Some(digest_answer) = digest_answer {
347                            if let Some(challenge) = &digest_answer.challenge {
348                                security_context.update_auth_info(
349                                    authentication_info_header,
350                                    host,
351                                    b"\"",
352                                    challenge,
353                                    false, // fix-me: we do have http body here
354                                );
355                            }
356                        }
357                    }
358
359                    return get_download_info(
360                        ft_http_cs_uri,
361                        tid,
362                        msisdn,
363                        http_client,
364                        gba_context,
365                        security_context,
366                        None,
367                    )
368                    .await;
369                } else if resp.status_code == 401 {
370                    if digest_answer.is_none() {
371                        if let Some(www_authenticate_header) =
372                            header::search(&resp.headers, b"WWW-Authenticate", false)
373                        {
374                            if let Some(Ok(answer)) = gba::try_process_401_response(
375                                gba_context,
376                                host.as_bytes(),
377                                conn.cipher_id(),
378                                PUT,
379                                b"\"/\"",
380                                None,
381                                www_authenticate_header,
382                                http_client,
383                                security_context,
384                            )
385                            .await
386                            {
387                                return resume_upload_put(
388                                    ft_http_cs_uri,
389                                    tid,
390                                    file,
391                                    resume_info,
392                                    msisdn,
393                                    http_client,
394                                    gba_context,
395                                    security_context,
396                                    Some(&answer),
397                                    progress_callback,
398                                )
399                                .await;
400                            }
401                        }
402                    }
403                } else {
404                    return Err(FileUploadError::Http(
405                        resp.status_code,
406                        match String::from_utf8(resp.reason_phrase) {
407                            Ok(reason_phrase) => reason_phrase,
408                            Err(_) => String::from(""),
409                        },
410                    ));
411                }
412            }
413        }
414
415        Err(FileUploadError::NetworkIO)
416    } else {
417        Err(FileUploadError::MalformedHost)
418    }
419}
420
421fn resume_upload_put<'a, 'b: 'a>(
422    ft_http_cs_uri: &'b str,
423    tid: Uuid,
424    file_info: FileInfo<'b>,
425    resume_info: &'b ResumeInfo,
426    msisdn: Option<&'b str>,
427    http_client: &'b Arc<HttpClient>,
428    gba_context: &'b Arc<GbaContext>,
429    security_context: &'b Arc<SecurityContext>,
430    digest_answer: Option<&'a DigestAnswerParams>,
431    progress_callback: &'b Arc<Box<dyn Fn(u32, i32) + Send + Sync>>,
432) -> BoxFuture<'a, Result<String, FileUploadError>> {
433    async move {
434        resume_upload_put_inner(
435            ft_http_cs_uri,
436            tid,
437            file_info,
438            resume_info,
439            msisdn,
440            http_client,
441            gba_context,
442            security_context,
443            digest_answer,
444            progress_callback,
445        )
446        .await
447    }
448    .boxed()
449}
450
451async fn resume_upload_check_inner(
452    ft_http_cs_uri: &str,
453    tid: Uuid,
454    file: FileInfo<'_>,
455    thumbnail: Option<FileInfo<'_>>,
456    msisdn: Option<&str>,
457    http_client: &Arc<HttpClient>,
458    gba_context: &Arc<GbaContext>,
459    security_context: &Arc<SecurityContext>,
460    digest_answer: Option<&DigestAnswerParams>,
461    progress_callback: &Arc<Box<dyn Fn(u32, i32) + Send + Sync>>,
462) -> Result<String, FileUploadError> {
463    let url_string = format!(
464        "{}?tid={}&get_upload_info",
465        ft_http_cs_uri,
466        tid.as_hyphenated().encode_lower(&mut Uuid::encode_buffer())
467    );
468
469    if let Ok(url) = Url::parse(&url_string) {
470        if let Ok(conn) = http_client.connect(&url, false).await {
471            let host = url.host_str().unwrap();
472
473            let mut req = Request::new_with_default_headers(GET, host, url.path(), url.query());
474
475            if let Some(msisdn) = msisdn {
476                req.headers.push(Header::new(
477                    b"X-3GPP-Intended-Identity",
478                    format!("tel:{}", msisdn),
479                ));
480            }
481
482            let preloaded_answer = match digest_answer {
483                Some(_) => None,
484                None => {
485                    platform_log(LOG_TAG, "using stored authorization info");
486                    security_context.preload_auth(gba_context, host, conn.cipher_id(), GET, None)
487                }
488            };
489
490            let digest_answer = match digest_answer {
491                Some(digest_answer) => Some(digest_answer),
492                None => match &preloaded_answer {
493                    Some(preloaded_answer) => {
494                        platform_log(LOG_TAG, "using preloaded digest answer");
495                        Some(preloaded_answer)
496                    }
497                    None => None,
498                },
499            };
500
501            if let Some(digest_answer) = digest_answer {
502                if let Ok(authorization) = digest_answer.make_authorization_header(
503                    match &digest_answer.challenge {
504                        Some(challenge) => Some(&challenge.algorithm),
505                        None => None,
506                    },
507                    false,
508                    false,
509                ) {
510                    if let Ok(authorization) = String::from_utf8(authorization) {
511                        req.headers
512                            .push(Header::new(b"Authorization", String::from(authorization)));
513                    }
514                }
515            }
516
517            if let Ok((resp, resp_stream)) = conn.send(req, |_| {}).await {
518                platform_log(
519                    LOG_TAG,
520                    format!(
521                        "resume_upload_check_inner resp.status_code = {}",
522                        resp.status_code
523                    ),
524                );
525
526                if resp.status_code == 200 {
527                    if let Some(authentication_info_header) =
528                        header::search(&resp.headers, b"Authentication-Info", false)
529                    {
530                        if let Some(digest_answer) = digest_answer {
531                            if let Some(challenge) = &digest_answer.challenge {
532                                security_context.update_auth_info(
533                                    authentication_info_header,
534                                    host,
535                                    b"\"",
536                                    challenge,
537                                    false,
538                                );
539                            }
540                        }
541                    }
542
543                    if let Some(mut resp_stream) = resp_stream {
544                        let mut resp_data = Vec::new();
545
546                        if let Ok(size) = resp_stream.read_to_end(&mut resp_data).await {
547                            platform_log(
548                                LOG_TAG,
549                                format!("resume_upload_check_inner resp.data read {} bytes", &size),
550                            );
551
552                            if let Some(resume_info) = parse_xml(&resp_data) {
553                                return resume_upload_put(
554                                    ft_http_cs_uri,
555                                    tid,
556                                    file,
557                                    &resume_info,
558                                    msisdn,
559                                    http_client,
560                                    gba_context,
561                                    security_context,
562                                    None,
563                                    progress_callback,
564                                )
565                                .await;
566                            }
567                        }
568                    }
569                } else if resp.status_code == 401 {
570                    if digest_answer.is_none() {
571                        if let Some(www_authenticate_header) =
572                            header::search(&resp.headers, b"WWW-Authenticate", false)
573                        {
574                            if let Some(Ok(answer)) = gba::try_process_401_response(
575                                gba_context,
576                                host.as_bytes(),
577                                conn.cipher_id(),
578                                GET,
579                                b"\"/\"",
580                                None,
581                                www_authenticate_header,
582                                http_client,
583                                security_context,
584                            )
585                            .await
586                            {
587                                return resume_upload_check(
588                                    ft_http_cs_uri,
589                                    tid,
590                                    file,
591                                    thumbnail,
592                                    msisdn,
593                                    http_client,
594                                    gba_context,
595                                    security_context,
596                                    Some(&answer),
597                                    progress_callback,
598                                )
599                                .await;
600                            }
601                        }
602                    }
603                } else if resp.status_code == 404 {
604                    return upload_file_inner(
605                        ft_http_cs_uri,
606                        tid,
607                        file,
608                        thumbnail,
609                        msisdn,
610                        http_client,
611                        gba_context,
612                        security_context,
613                        progress_callback,
614                    )
615                    .await;
616                } else {
617                    return Err(FileUploadError::Http(
618                        resp.status_code,
619                        match String::from_utf8(resp.reason_phrase) {
620                            Ok(reason_phrase) => reason_phrase,
621                            Err(_) => String::from(""),
622                        },
623                    ));
624                }
625            }
626        }
627
628        Err(FileUploadError::NetworkIO)
629    } else {
630        Err(FileUploadError::MalformedHost)
631    }
632}
633
634fn resume_upload_check<'a, 'b: 'a>(
635    ft_http_cs_uri: &'b str,
636    tid: Uuid,
637    file: FileInfo<'b>,
638    thumbnail: Option<FileInfo<'b>>,
639    msisdn: Option<&'b str>,
640    http_client: &'b Arc<HttpClient>,
641    gba_context: &'b Arc<GbaContext>,
642    security_context: &'b Arc<SecurityContext>,
643    digest_answer: Option<&'a DigestAnswerParams>,
644    progress_callback: &'b Arc<Box<dyn Fn(u32, i32) + Send + Sync>>,
645) -> BoxFuture<'a, Result<String, FileUploadError>> {
646    async move {
647        resume_upload_check_inner(
648            ft_http_cs_uri,
649            tid,
650            file,
651            thumbnail,
652            msisdn,
653            http_client,
654            gba_context,
655            security_context,
656            digest_answer,
657            progress_callback,
658        )
659        .await
660    }
661    .boxed()
662}
663
664async fn resume_upload(
665    ft_http_cs_uri: &str,
666    tid: Uuid,
667    file: FileInfo<'_>,
668    thumbnail: Option<FileInfo<'_>>,
669    msisdn: Option<&str>,
670    http_client: &Arc<HttpClient>,
671    gba_context: &Arc<GbaContext>,
672    security_context: &Arc<SecurityContext>,
673    progress_callback: &Arc<Box<dyn Fn(u32, i32) + Send + Sync>>,
674) -> Result<String, FileUploadError> {
675    resume_upload_check(
676        ft_http_cs_uri,
677        tid,
678        file,
679        thumbnail,
680        msisdn,
681        http_client,
682        gba_context,
683        security_context,
684        None,
685        progress_callback,
686    )
687    .await
688}
689
690async fn upload_file_actual_content_post_inner(
691    url: &Url,
692    conn: &HttpConnectionHandle,
693    tid: Uuid,
694    file: FileInfo<'_>,
695    thumbnail: Option<FileInfo<'_>>,
696    msisdn: Option<&str>,
697    http_client: &Arc<HttpClient>,
698    gba_context: &Arc<GbaContext>,
699    security_context: &Arc<SecurityContext>,
700    digest_answer: Option<&DigestAnswerParams>,
701    progress_callback: &Arc<Box<dyn Fn(u32, i32) + Send + Sync>>,
702) -> Result<String, FileUploadError> {
703    let host = url.host_str().unwrap();
704
705    let mut req = Request::new_with_default_headers(POST, host, url.path(), url.query());
706
707    if let Some(msisdn) = msisdn {
708        req.headers.push(Header::new(
709            b"X-3GPP-Intended-Identity",
710            format!("tel:{}", msisdn),
711        ));
712    }
713
714    let preloaded_answer = match digest_answer {
715        Some(_) => None,
716        None => {
717            platform_log(LOG_TAG, "using stored authorization info");
718            security_context.preload_auth(gba_context, host, conn.cipher_id(), POST, None)
719            // fix-me: we do have to perform digest here
720        }
721    };
722
723    let digest_answer = match digest_answer {
724        Some(digest_answer) => Some(digest_answer),
725        None => match &preloaded_answer {
726            Some(preloaded_answer) => {
727                platform_log(LOG_TAG, "using preloaded digest answer");
728                Some(preloaded_answer)
729            }
730            None => None,
731        },
732    };
733
734    if let Some(digest_answer) = digest_answer {
735        if let Ok(authorization) = digest_answer.make_authorization_header(
736            match &digest_answer.challenge {
737                Some(challenge) => Some(&challenge.algorithm),
738                None => None,
739            },
740            false,
741            false,
742        ) {
743            if let Ok(authorization) = String::from_utf8(authorization) {
744                req.headers
745                    .push(Header::new(b"Authorization", String::from(authorization)));
746            }
747        }
748    }
749
750    let boundary = create_raw_alpha_numeric_string(16);
751    let boundary_ = String::from_utf8_lossy(&boundary);
752
753    let tid_string = String::from(tid.as_hyphenated().encode_lower(&mut Uuid::encode_buffer()));
754
755    let tid_payload = Body::Message(MessageBody {
756        headers: [
757            Header::new("Content-Disposition", "form-data; name=\"tid\""),
758            Header::new("Content-Type", "text/plain; charset=utf-8"),
759            Header::new("Content-Length", format!("{}", tid_string.len())),
760        ]
761        .to_vec(),
762        body: Arc::new(Body::Raw(tid_string.into_bytes())),
763    });
764
765    let thumbnail_payload = match &thumbnail {
766        Some(thumbnail) => {
767            let mut file_size = 0;
768            if let Ok(mut f) = File::open(thumbnail.path) {
769                if let Ok(meta) = f.metadata() {
770                    file_size = meta.len();
771                } else {
772                    if let Ok(size) = f.seek(std::io::SeekFrom::End(0)) {
773                        file_size = size;
774                    }
775                }
776            }
777
778            let stream_size = match usize::try_from(file_size) {
779                Ok(file_size) => {
780                    if file_size > 0 {
781                        file_size
782                    } else {
783                        return Err(FileUploadError::IO);
784                    }
785                }
786                Err(_) => return Err(FileUploadError::IO),
787            };
788
789            Some(Body::Message(MessageBody {
790                headers: [
791                    Header::new(
792                        "Content-Disposition",
793                        format!(
794                            "form-data; name=\"Thumbnail\"; filename=\"{}\"",
795                            thumbnail.name
796                        ),
797                    ),
798                    Header::new("Content-Type", String::from(thumbnail.mime)),
799                    Header::new("Content-Transfer-Encoding", "binary"),
800                    Header::new(
801                        "Content-Range",
802                        format!("0-{}/{}", stream_size - 1, stream_size),
803                    ),
804                    Header::new("Content-Length", format!("{}", stream_size)),
805                ]
806                .to_vec(),
807                body: Arc::new(Body::Streamed(StreamedBody {
808                    stream_size,
809                    stream_source: StreamSource::File((String::from(thumbnail.path), 0)),
810                })),
811            }))
812        }
813        None => None,
814    };
815
816    let mut file_size = 0;
817    if let Ok(mut f) = File::open(file.path) {
818        if let Ok(meta) = f.metadata() {
819            file_size = meta.len();
820        } else {
821            if let Ok(size) = f.seek(std::io::SeekFrom::End(0)) {
822                file_size = size;
823            }
824        }
825    }
826
827    let stream_size = match usize::try_from(file_size) {
828        Ok(file_size) => {
829            if file_size > 0 {
830                file_size
831            } else {
832                return Err(FileUploadError::IO);
833            }
834        }
835        Err(_) => return Err(FileUploadError::IO),
836    };
837
838    let file_payload = Body::Message(MessageBody {
839        headers: [
840            Header::new(
841                "Content-Disposition",
842                format!("form-data; name=\"File\"; filename=\"{}\"", file.name),
843            ),
844            Header::new("Content-Type", String::from(file.mime)),
845            Header::new("Content-Transfer-Encoding", "binary"),
846            Header::new(
847                "Content-Range",
848                format!("0-{}/{}", stream_size - 1, stream_size),
849            ),
850            Header::new("Content-Length", format!("{}", stream_size)),
851        ]
852        .to_vec(),
853        body: Arc::new(Body::Streamed(StreamedBody {
854            stream_size,
855            stream_source: StreamSource::File((String::from(file.path), 0)),
856        })),
857    });
858
859    req.headers.push(Header::new(
860        "Content-Type",
861        format!("multipart/form-data; boundary={}", boundary_),
862    ));
863
864    let http_body = Body::Multipart(MultipartBody {
865        boundary,
866        parts: match thumbnail_payload {
867            Some(thumbnail_payload) => [
868                Arc::new(tid_payload),
869                Arc::new(thumbnail_payload),
870                Arc::new(file_payload),
871            ]
872            .to_vec(),
873            None => [Arc::new(tid_payload), Arc::new(file_payload)].to_vec(),
874        },
875    });
876
877    let progress_total = http_body.estimated_size();
878
879    req.headers
880        .push(Header::new("Content-Length", format!("{}", progress_total)));
881
882    req.body = Some(http_body);
883
884    let progress_callback_ = Arc::clone(progress_callback);
885
886    if let Ok((resp, resp_stream)) = conn
887        .send(req, move |written| {
888            if let Ok(current) = u32::try_from(written) {
889                if let Ok(total) = i32::try_from(progress_total) {
890                    progress_callback_(current, total);
891                } else {
892                    progress_callback_(current, -1);
893                }
894            }
895        })
896        .await
897    {
898        platform_log(
899            LOG_TAG,
900            format!(
901                "upload_file_actual_content_post_inner resp.status_code = {}",
902                resp.status_code
903            ),
904        );
905
906        if resp.status_code == 200 {
907            if let Some(authentication_info_header) =
908                header::search(&resp.headers, b"Authentication-Info", false)
909            {
910                if let Some(digest_answer) = digest_answer {
911                    if let Some(challenge) = &digest_answer.challenge {
912                        security_context.update_auth_info(
913                            authentication_info_header,
914                            host,
915                            b"\"",
916                            challenge,
917                            false, // fix-me: we do have http body here
918                        );
919                    }
920                }
921            }
922
923            if let Some(mut resp_stream) = resp_stream {
924                let mut resp_data = Vec::new();
925                if let Ok(size) = resp_stream.read_to_end(&mut resp_data).await {
926                    platform_log(
927                        LOG_TAG,
928                        format!(
929                            "upload_file_actual_content_post_inner resp.data read {} bytes",
930                            &size
931                        ),
932                    );
933
934                    if let Ok(resp_string) = String::from_utf8(resp_data) {
935                        platform_log(
936                            LOG_TAG,
937                            format!(
938                                "upload_file_actual_content_post_inner resp.string = {}",
939                                &resp_string
940                            ),
941                        );
942
943                        return Ok(resp_string);
944                    }
945                }
946            }
947        } else {
948            return Err(FileUploadError::Http(
949                resp.status_code,
950                match String::from_utf8(resp.reason_phrase) {
951                    Ok(reason_phrase) => reason_phrase,
952                    Err(_) => String::from(""),
953                },
954            ));
955        }
956    }
957
958    Err(FileUploadError::MalformedHost)
959}
960
961fn upload_file_actual_content_post<'a, 'b: 'a>(
962    url: &'a Url,
963    conn: &'a HttpConnectionHandle,
964    tid: Uuid,
965    file: FileInfo<'b>,
966    thumbnail: Option<FileInfo<'b>>,
967    msisdn: Option<&'b str>,
968    http_client: &'b Arc<HttpClient>,
969    gba_context: &'b Arc<GbaContext>,
970    security_context: &'b Arc<SecurityContext>,
971    digest_answer: Option<&'a DigestAnswerParams>,
972    progress_callback: &'b Arc<Box<dyn Fn(u32, i32) + Send + Sync>>,
973) -> BoxFuture<'a, Result<String, FileUploadError>> {
974    async move {
975        upload_file_actual_content_post_inner(
976            url,
977            conn,
978            tid,
979            file,
980            thumbnail,
981            msisdn,
982            http_client,
983            gba_context,
984            security_context,
985            digest_answer,
986            progress_callback,
987        )
988        .await
989    }
990    .boxed()
991}
992
993async fn upload_file_initial_empty_post_inner(
994    url: &Url,
995    conn: &HttpConnectionHandle,
996    tid: Uuid,
997    file: FileInfo<'_>,
998    thumbnail: Option<FileInfo<'_>>,
999    msisdn: Option<&str>,
1000    http_client: &Arc<HttpClient>,
1001    gba_context: &Arc<GbaContext>,
1002    security_context: &Arc<SecurityContext>,
1003    progress_callback: &Arc<Box<dyn Fn(u32, i32) + Send + Sync>>,
1004) -> Result<String, FileUploadError> {
1005    let host = url.host_str().unwrap();
1006
1007    let mut req = Request::new_with_default_headers(POST, host, url.path(), url.query());
1008
1009    if let Some(msisdn) = msisdn {
1010        req.headers.push(Header::new(
1011            b"X-3GPP-Intended-Identity",
1012            format!("tel:{}", msisdn),
1013        ));
1014    }
1015
1016    if let Ok((resp, _)) = conn.send(req, |_| {}).await {
1017        platform_log(
1018            LOG_TAG,
1019            format!(
1020                "upload_file_initial_empty_post_inner resp.status_code = {}",
1021                resp.status_code
1022            ),
1023        );
1024
1025        if resp.status_code == 204 {
1026            return upload_file_actual_content_post(
1027                url,
1028                conn,
1029                tid,
1030                file,
1031                thumbnail,
1032                msisdn,
1033                http_client,
1034                gba_context,
1035                security_context,
1036                None,
1037                progress_callback,
1038            )
1039            .await;
1040        } else if resp.status_code == 401 {
1041            if let Some(www_authenticate_header) =
1042                header::search(&resp.headers, b"WWW-Authenticate", false)
1043            {
1044                // to-do: could also be a simple Digest with ftHTTPCSUser and ftHTTPCSPwd
1045                if let Some(Ok(authorization)) = gba::try_process_401_response(
1046                    gba_context,
1047                    host.as_bytes(),
1048                    conn.cipher_id(),
1049                    POST,
1050                    b"\"/\"",
1051                    None,
1052                    www_authenticate_header,
1053                    http_client,
1054                    security_context,
1055                )
1056                .await
1057                {
1058                    return upload_file_actual_content_post(
1059                        &url,
1060                        &conn,
1061                        tid,
1062                        file,
1063                        thumbnail,
1064                        msisdn,
1065                        http_client,
1066                        gba_context,
1067                        security_context,
1068                        Some(&authorization),
1069                        progress_callback,
1070                    )
1071                    .await;
1072                }
1073            }
1074        } else {
1075            return Err(FileUploadError::Http(
1076                resp.status_code,
1077                match String::from_utf8(resp.reason_phrase) {
1078                    Ok(reason_phrase) => reason_phrase,
1079                    Err(_) => String::from(""),
1080                },
1081            ));
1082        }
1083    }
1084
1085    Err(FileUploadError::NetworkIO)
1086}
1087
1088fn upload_file_initial_empty_post<'a, 'b: 'a>(
1089    url: &'a Url,
1090    conn: &'a HttpConnectionHandle,
1091    tid: Uuid,
1092    file: FileInfo<'b>,
1093    thumbnail: Option<FileInfo<'b>>,
1094    msisdn: Option<&'b str>,
1095    http_client: &'b Arc<HttpClient>,
1096    gba_context: &'b Arc<GbaContext>,
1097    security_context: &'b Arc<SecurityContext>,
1098    progress_callback: &'b Arc<Box<dyn Fn(u32, i32) + Send + Sync>>,
1099) -> BoxFuture<'a, Result<String, FileUploadError>> {
1100    async move {
1101        upload_file_initial_empty_post_inner(
1102            url,
1103            conn,
1104            tid,
1105            file,
1106            thumbnail,
1107            msisdn,
1108            http_client,
1109            gba_context,
1110            security_context,
1111            progress_callback,
1112        )
1113        .await
1114    }
1115    .boxed()
1116}
1117
1118async fn upload_file_inner(
1119    ft_http_cs_uri: &str,
1120    tid: Uuid,
1121    file: FileInfo<'_>,
1122    thumbnail: Option<FileInfo<'_>>,
1123    msisdn: Option<&str>,
1124    http_client: &Arc<HttpClient>,
1125    gba_context: &Arc<GbaContext>,
1126    security_context: &Arc<SecurityContext>,
1127    progress_callback: &Arc<Box<dyn Fn(u32, i32) + Send + Sync>>,
1128) -> Result<String, FileUploadError> {
1129    platform_log(LOG_TAG, "calling upload_file_inner()");
1130
1131    if let Ok(url) = Url::parse(ft_http_cs_uri) {
1132        if let Ok(conn) = http_client.connect(&url, false).await {
1133            return upload_file_initial_empty_post(
1134                &url,
1135                &conn,
1136                tid,
1137                file,
1138                thumbnail,
1139                msisdn,
1140                http_client,
1141                gba_context,
1142                security_context,
1143                progress_callback,
1144            )
1145            .await;
1146        }
1147
1148        Err(FileUploadError::NetworkIO)
1149    } else {
1150        Err(FileUploadError::MalformedHost)
1151    }
1152}
1153
1154pub struct FileInfo<'a> {
1155    pub path: &'a str,
1156    pub name: &'a str,
1157    pub mime: &'a str,
1158    pub hash: Option<&'a str>,
1159}
1160
1161pub fn upload_file<'a, 'b: 'a>(
1162    ft_http_service: &'b Arc<FileTransferOverHTTPService>,
1163    ft_http_cs_uri: &'b str,
1164    tid: Uuid,
1165    file: FileInfo<'b>,
1166    thumbnail: Option<FileInfo<'b>>,
1167    msisdn: Option<&'b str>,
1168    http_client: &'b Arc<HttpClient>,
1169    gba_context: &'b Arc<GbaContext>,
1170    security_context: &'b Arc<SecurityContext>,
1171    progress_callback: &'b Arc<Box<dyn Fn(u32, i32) + Send + Sync>>,
1172) -> BoxFuture<'a, Result<String, FileUploadError>> {
1173    let mut is_known_task = false;
1174
1175    {
1176        let mut guard = ft_http_service.recorded_tids.lock().unwrap();
1177        for recorded_tid in &*guard {
1178            if &tid == recorded_tid {
1179                is_known_task = true;
1180                break;
1181            }
1182        }
1183
1184        if !is_known_task {
1185            guard.push(tid);
1186        }
1187    }
1188
1189    if is_known_task {
1190        async move {
1191            resume_upload(
1192                ft_http_cs_uri,
1193                tid,
1194                file,
1195                thumbnail,
1196                msisdn,
1197                http_client,
1198                gba_context,
1199                security_context,
1200                progress_callback,
1201            )
1202            .await
1203        }
1204        .boxed()
1205    } else {
1206        async move {
1207            upload_file_inner(
1208                ft_http_cs_uri,
1209                tid,
1210                file,
1211                thumbnail,
1212                msisdn,
1213                http_client,
1214                gba_context,
1215                security_context,
1216                progress_callback,
1217            )
1218            .await
1219        }
1220        .boxed()
1221    }
1222}