1use 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 }
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, );
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 }
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, );
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 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}