1pub mod chatbot_config;
16pub mod chatbot_sip_uri;
17pub mod cpim;
18pub mod ffi;
19
20use std::sync::Arc;
21
22use futures::{future::BoxFuture, AsyncReadExt, FutureExt};
23use rust_rcs_core::{
24 ffi::log::platform_log,
25 http::{
26 request::{Request, GET},
27 HttpClient,
28 },
29 internet::{header, headers::cache_control, Header},
30 security::{
31 authentication::digest::DigestAnswerParams,
32 gba::{self, GbaContext},
33 SecurityContext,
34 },
35};
36use url::Url;
37
38const LOG_TAG: &str = "chat_bot";
39
40pub enum RetrieveSpecificChatbotsSuccess {
41 Ok(String, Option<String>, u32),
42 NotModified(Option<String>, u32),
43}
44
45#[derive(Debug)]
46pub enum RetrieveSpecificChatbotsError {
47 Http(u16, String),
48 NetworkIO,
49 MalformedUrl,
50}
51
52impl RetrieveSpecificChatbotsError {
53 pub fn error_code(&self) -> u16 {
54 match self {
55 RetrieveSpecificChatbotsError::Http(status_code, _) => *status_code,
56 RetrieveSpecificChatbotsError::NetworkIO => 0,
57 RetrieveSpecificChatbotsError::MalformedUrl => 0,
58 }
59 }
60
61 pub fn error_string(&self) -> String {
62 match self {
63 RetrieveSpecificChatbotsError::Http(_, reason_phrase) => String::from(reason_phrase),
64 RetrieveSpecificChatbotsError::NetworkIO => String::from("NetworkIO"),
65 RetrieveSpecificChatbotsError::MalformedUrl => String::from("MalformedUrl"),
66 }
67 }
68}
69
70async fn retrieve_specific_chatbots_inner(
71 specific_chatbots_list_url: &str,
72 local_etag: Option<&str>,
73 msisdn: Option<&str>,
74 http_client: &Arc<HttpClient>,
75 gba_context: &Arc<GbaContext>,
76 security_context: &Arc<SecurityContext>,
77 digest_answer: Option<&DigestAnswerParams>,
78) -> Result<RetrieveSpecificChatbotsSuccess, RetrieveSpecificChatbotsError> {
79 if let Ok(url) = Url::parse(specific_chatbots_list_url) {
80 if let Ok(conn) = http_client.connect(&url, false).await {
81 let host = url.host_str().unwrap();
82
83 let mut req = Request::new_with_default_headers(GET, host, url.path(), url.query());
84
85 if let Some(msisdn) = msisdn {
86 req.headers.push(Header::new(
87 b"X-3GPP-Intended-Identity",
88 format!("tel:{}", msisdn),
89 ));
90 }
91
92 if let Some(local_etag) = local_etag {
93 req.headers
94 .push(Header::new(b"If-None-Match", String::from(local_etag)));
95 }
96
97 let preloaded_answer = match digest_answer {
98 Some(_) => None,
99 None => {
100 platform_log(LOG_TAG, "using stored authorization info");
101 security_context.preload_auth(gba_context, host, conn.cipher_id(), GET, None)
102 }
103 };
104
105 let digest_answer = match digest_answer {
106 Some(digest_answer) => Some(digest_answer),
107 None => match &preloaded_answer {
108 Some(preloaded_answer) => {
109 platform_log(LOG_TAG, "using preloaded digest answer");
110 Some(preloaded_answer)
111 }
112 None => None,
113 },
114 };
115
116 if let Some(digest_answer) = digest_answer {
117 if let Ok(authorization) = digest_answer.make_authorization_header(
118 match &digest_answer.challenge {
119 Some(challenge) => Some(&challenge.algorithm),
120 None => None,
121 },
122 false,
123 false,
124 ) {
125 if let Ok(authorization) = String::from_utf8(authorization) {
126 req.headers
127 .push(Header::new(b"Authorization", String::from(authorization)));
128 }
129 }
130 }
131
132 if let Ok((resp, resp_stream)) = conn.send(req, |_| {}).await {
133 platform_log(
134 LOG_TAG,
135 format!(
136 "retrieve_specific_chatbots_inner resp.status_code = {}",
137 resp.status_code
138 ),
139 );
140
141 if (resp.status_code >= 200 && resp.status_code < 300) || resp.status_code == 304 {
142 if let Some(authentication_info_header) =
143 header::search(&resp.headers, b"Authentication-Info", false)
144 {
145 if let Some(digest_answer) = digest_answer {
146 if let Some(challenge) = &digest_answer.challenge {
147 security_context.update_auth_info(
148 authentication_info_header,
149 host,
150 b"\"",
151 challenge,
152 false,
153 );
154 }
155 }
156 }
157 }
158
159 if resp.status_code == 200 || resp.status_code == 304 {
160 let response_etag =
161 if let Some(etag_header) = header::search(&resp.headers, b"ETag", false) {
162 match String::from_utf8(etag_header.get_value().to_vec()) {
163 Ok(etag) => Some(etag),
164 Err(_) => None,
165 }
166 } else {
167 None
168 };
169
170 let expiry = if let Some(cache_control_header) =
171 header::search(&resp.headers, b"Cache-Control", false)
172 {
173 let cc = cache_control::parse(cache_control_header.get_value());
174 cc.max_age
175 } else {
176 0
177 };
178
179 if resp.status_code == 200 {
180 if let Some(mut resp_stream) = resp_stream {
181 let mut resp_data = Vec::new();
182 match resp_stream.read_to_end(&mut resp_data).await {
183 Ok(size) => {
184 platform_log(
185 LOG_TAG,
186 format!(
187 "retrieve_specific_chatbots_inner resp.data read {} bytes",
188 &size
189 ),
190 );
191
192 if let Ok(resp_string) = String::from_utf8(resp_data) {
193 platform_log(
194 LOG_TAG,
195 format!(
196 "retrieve_specific_chatbots_inner resp.string = {}",
197 &resp_string
198 ),
199 );
200
201 return Ok(RetrieveSpecificChatbotsSuccess::Ok(
202 resp_string,
203 response_etag,
204 expiry,
205 ));
206 }
207 }
208
209 Err(e) => {
210 platform_log(LOG_TAG, format!("retrieve_specific_chatbots_inner error reading response stream: {:?}", e));
211 }
212 }
213 } else {
214 platform_log(
215 LOG_TAG,
216 "retrieve_specific_chatbots_inner missing response body",
217 );
218 }
219 } else {
220 return Ok(RetrieveSpecificChatbotsSuccess::NotModified(
221 response_etag,
222 expiry,
223 ));
224 }
225 } else if resp.status_code == 401 {
226 if digest_answer.is_none() {
227 if let Some(www_authenticate_header) =
228 header::search(&resp.headers, b"WWW-Authenticate", false)
229 {
230 if let Some(Ok(answer)) = gba::try_process_401_response(
231 gba_context,
232 host.as_bytes(),
233 conn.cipher_id(),
234 GET,
235 b"\"/\"",
236 None,
237 www_authenticate_header,
238 http_client,
239 security_context,
240 )
241 .await
242 {
243 return retrieve_specific_chatbots(
244 specific_chatbots_list_url,
245 local_etag,
246 msisdn,
247 http_client,
248 gba_context,
249 security_context,
250 Some(&answer),
251 )
252 .await;
253 }
254 }
255 }
256 } else {
257 return Err(RetrieveSpecificChatbotsError::Http(
258 resp.status_code,
259 match String::from_utf8(resp.reason_phrase) {
260 Ok(reason_phrase) => reason_phrase,
261 Err(_) => String::from(""),
262 },
263 ));
264 }
265 }
266 }
267
268 Err(RetrieveSpecificChatbotsError::NetworkIO)
269 } else {
270 Err(RetrieveSpecificChatbotsError::MalformedUrl)
271 }
272}
273
274pub fn retrieve_specific_chatbots<'a, 'b: 'a>(
275 specific_chatbots_list_url: &'b str,
276 local_etag: Option<&'b str>,
277 msisdn: Option<&'b str>,
278 http_client: &'b Arc<HttpClient>,
279 gba_context: &'b Arc<GbaContext>,
280 security_context: &'b Arc<SecurityContext>,
281 digest_answer: Option<&'a DigestAnswerParams>,
282) -> BoxFuture<'a, Result<RetrieveSpecificChatbotsSuccess, RetrieveSpecificChatbotsError>> {
283 async move {
284 retrieve_specific_chatbots_inner(
285 specific_chatbots_list_url,
286 local_etag,
287 msisdn,
288 http_client,
289 gba_context,
290 security_context,
291 digest_answer,
292 )
293 .await
294 }
295 .boxed()
296}
297
298#[derive(Debug)]
299pub enum SearchChatbotError {
300 Http(u16, String),
301 NetworkIO,
302 MalformedUrl,
303}
304
305impl SearchChatbotError {
306 pub fn error_code(&self) -> u16 {
307 match self {
308 SearchChatbotError::Http(status_code, _) => *status_code,
309 SearchChatbotError::NetworkIO => 0,
310 SearchChatbotError::MalformedUrl => 0,
311 }
312 }
313
314 pub fn error_string(&self) -> String {
315 match self {
316 SearchChatbotError::Http(_, reason_phrase) => String::from(reason_phrase),
317 SearchChatbotError::NetworkIO => String::from("NetworkIO"),
318 SearchChatbotError::MalformedUrl => String::from("MalformedUrl"),
319 }
320 }
321}
322
323async fn search_chatbot_directory_inner(
324 chatbot_directory: &str,
325 query: &str,
326 start: u32,
327 num: u32,
328 home_operator: &str,
329 msisdn: Option<&str>,
330 http_client: &Arc<HttpClient>,
331 gba_context: &Arc<GbaContext>,
332 security_context: &Arc<SecurityContext>,
333 digest_answer: Option<&'_ DigestAnswerParams>,
334) -> Result<String, SearchChatbotError> {
335 let mut first_query = true; let mut url_string = String::from(chatbot_directory);
337 if query.len() != 0 {
338 url_string = format!("{}?q={}", url_string, query);
339 first_query = false;
340 }
341
342 if start != 0 {
343 url_string = if first_query {
344 format!("{}?start={}", url_string, query)
345 } else {
346 format!("{}&start={}", url_string, query)
347 };
348 first_query = false;
349 }
350
351 url_string = if first_query {
352 format!(
353 "{}&num={}&ho={}&chatbot_version=1_2&client_vendor=Rusty&client_version=1.0.0",
354 url_string, num, home_operator
355 )
356 } else {
357 format!(
358 "{}&num={}&ho={}&chatbot_version=1_2&client_vendor=Rusty&client_version=1.0.0",
359 url_string, num, home_operator
360 )
361 };
362
363 platform_log(LOG_TAG, format!("url: {}", &url_string));
364
365 if let Ok(url) = Url::parse(&url_string) {
366 if let Ok(conn) = http_client.connect(&url, false).await {
367 let host = url.host_str().unwrap();
368
369 let mut req = Request::new_with_default_headers(GET, host, url.path(), url.query());
370
371 if let Some(msisdn) = msisdn {
372 req.headers.push(Header::new(
373 b"X-3GPP-Intended-Identity",
374 format!("tel:{}", msisdn),
375 ));
376 }
377
378 let preloaded_answer = match digest_answer {
379 Some(_) => None,
380 None => {
381 platform_log(LOG_TAG, "using stored authorization info");
382 security_context.preload_auth(gba_context, host, conn.cipher_id(), GET, None)
383 }
384 };
385
386 let digest_answer = match digest_answer {
387 Some(digest_answer) => Some(digest_answer),
388 None => match &preloaded_answer {
389 Some(preloaded_answer) => {
390 platform_log(LOG_TAG, "using preloaded digest answer");
391 Some(preloaded_answer)
392 }
393 None => None,
394 },
395 };
396
397 if let Some(digest_answer) = digest_answer {
398 if let Ok(authorization) = digest_answer.make_authorization_header(
399 match &digest_answer.challenge {
400 Some(challenge) => Some(&challenge.algorithm),
401 None => None,
402 },
403 false,
404 false,
405 ) {
406 if let Ok(authorization) = String::from_utf8(authorization) {
407 req.headers
408 .push(Header::new(b"Authorization", String::from(authorization)));
409 }
410 }
411 }
412
413 if let Ok((resp, resp_stream)) = conn.send(req, |_| {}).await {
414 platform_log(
415 LOG_TAG,
416 format!(
417 "search_chatbot_directory_inner resp.status_code = {}",
418 resp.status_code
419 ),
420 );
421
422 if resp.status_code == 200 {
423 if let Some(authentication_info_header) =
424 header::search(&resp.headers, b"Authentication-Info", false)
425 {
426 if let Some(digest_answer) = digest_answer {
427 if let Some(challenge) = &digest_answer.challenge {
428 security_context.update_auth_info(
429 authentication_info_header,
430 host,
431 b"\"",
432 challenge,
433 false,
434 );
435 }
436 }
437 }
438
439 if let Some(mut resp_stream) = resp_stream {
440 let mut resp_data = Vec::new();
441 match resp_stream.read_to_end(&mut resp_data).await {
442 Ok(size) => {
443 platform_log(
444 LOG_TAG,
445 format!(
446 "search_chatbot_directory_inner resp.data read {} bytes",
447 &size
448 ),
449 );
450
451 if let Ok(resp_string) = String::from_utf8(resp_data) {
452 platform_log(
453 LOG_TAG,
454 format!(
455 "search_chatbot_directory_inner resp.string = {}",
456 &resp_string
457 ),
458 );
459
460 return Ok(resp_string);
461 }
462 }
463
464 Err(e) => {
465 platform_log(LOG_TAG, format!("search_chatbot_directory_inner error reading response stream: {:?}", e));
466 }
467 }
468 } else {
469 platform_log(
470 LOG_TAG,
471 "search_chatbot_directory_inner missing response body",
472 );
473 }
474 } else if resp.status_code == 401 {
475 if digest_answer.is_none() {
476 if let Some(www_authenticate_header) =
477 header::search(&resp.headers, b"WWW-Authenticate", false)
478 {
479 if let Some(Ok(answer)) = gba::try_process_401_response(
480 gba_context,
481 host.as_bytes(),
482 conn.cipher_id(),
483 GET,
484 b"\"/\"",
485 None,
486 www_authenticate_header,
487 http_client,
488 security_context,
489 )
490 .await
491 {
492 return search_chatbot_directory(
493 chatbot_directory,
494 query,
495 start,
496 num,
497 home_operator,
498 msisdn,
499 http_client,
500 gba_context,
501 security_context,
502 Some(&answer),
503 )
504 .await;
505 }
506 }
507 }
508 } else {
509 return Err(SearchChatbotError::Http(
510 resp.status_code,
511 match String::from_utf8(resp.reason_phrase) {
512 Ok(reason_phrase) => reason_phrase,
513 Err(_) => String::from(""),
514 },
515 ));
516 }
517 }
518 }
519
520 Err(SearchChatbotError::NetworkIO)
521 } else {
522 Err(SearchChatbotError::MalformedUrl)
523 }
524}
525
526pub fn search_chatbot_directory<'a, 'b: 'a>(
527 chatbot_directory: &'b str,
528 query: &'b str,
529 start: u32,
530 num: u32,
531 home_operator: &'b str,
532 msisdn: Option<&'b str>,
533 http_client: &'b Arc<HttpClient>,
534 gba_context: &'b Arc<GbaContext>,
535 security_context: &'b Arc<SecurityContext>,
536 digest_answer: Option<&'a DigestAnswerParams>,
537) -> BoxFuture<'a, Result<String, SearchChatbotError>> {
538 async move {
539 search_chatbot_directory_inner(
540 chatbot_directory,
541 query,
542 start,
543 num,
544 home_operator,
545 msisdn,
546 http_client,
547 gba_context,
548 security_context,
549 digest_answer,
550 )
551 .await
552 }
553 .boxed()
554}
555
556pub enum RetrieveChatbotInfoSuccess {
557 Ok(String, Option<String>, u32),
558 NotModified(Option<String>, u32),
559}
560
561#[derive(Debug)]
562pub enum RetrieveChatbotInfoError {
563 Http(u16, String),
564 NetworkIO,
565 MalformedUrl,
566}
567
568impl RetrieveChatbotInfoError {
569 pub fn error_code(&self) -> u16 {
570 match self {
571 RetrieveChatbotInfoError::Http(status_code, _) => *status_code,
572 RetrieveChatbotInfoError::NetworkIO => 0,
573 RetrieveChatbotInfoError::MalformedUrl => 0,
574 }
575 }
576
577 pub fn error_string(&self) -> String {
578 match self {
579 RetrieveChatbotInfoError::Http(_, reason_phrase) => String::from(reason_phrase),
580 RetrieveChatbotInfoError::NetworkIO => String::from("NetworkIO"),
581 RetrieveChatbotInfoError::MalformedUrl => String::from("MalformedUrl"),
582 }
583 }
584}
585
586async fn retrieve_chatbot_info_inner(
587 bot_domain: &str,
588 bot_id: &str,
589 local_etag: Option<&str>,
590 home_operator: &str,
591 home_language: &str,
592 msisdn: Option<&str>,
593 http_client: &Arc<HttpClient>,
594 gba_context: &Arc<GbaContext>,
595 security_context: &Arc<SecurityContext>,
596 digest_answer: Option<&DigestAnswerParams>,
597) -> Result<RetrieveChatbotInfoSuccess, RetrieveChatbotInfoError> {
598 let url_string = format!(
599 "https://{}/bot?id={}&hl={}&ho={}&v=3",
600 bot_domain,
601 urlencoding::encode(bot_id),
602 home_language,
603 home_operator
604 );
605
606 if let Ok(url) = Url::parse(&url_string) {
607 if let Ok(conn) = http_client.connect(&url, false).await {
608 let host = url.host_str().unwrap();
609
610 let mut req = Request::new_with_default_headers(GET, host, url.path(), url.query());
611
612 if let Some(msisdn) = msisdn {
613 req.headers.push(Header::new(
614 b"X-3GPP-Intended-Identity",
615 format!("tel:{}", msisdn),
616 ));
617 }
618
619 if let Some(local_etag) = local_etag {
620 req.headers
621 .push(Header::new(b"If-None-Match", String::from(local_etag)));
622 }
623
624 let preloaded_answer = match digest_answer {
625 Some(_) => None,
626 None => {
627 platform_log(LOG_TAG, "using stored authorization info");
628 security_context.preload_auth(gba_context, host, conn.cipher_id(), GET, None)
629 }
630 };
631
632 let digest_answer = match digest_answer {
633 Some(digest_answer) => Some(digest_answer),
634 None => match &preloaded_answer {
635 Some(preloaded_answer) => {
636 platform_log(LOG_TAG, "using preloaded digest answer");
637 Some(preloaded_answer)
638 }
639 None => None,
640 },
641 };
642
643 if let Some(digest_answer) = digest_answer {
644 if let Ok(authorization) = digest_answer.make_authorization_header(
645 match &digest_answer.challenge {
646 Some(challenge) => Some(&challenge.algorithm),
647 None => None,
648 },
649 false,
650 false,
651 ) {
652 if let Ok(authorization) = String::from_utf8(authorization) {
653 req.headers
654 .push(Header::new(b"Authorization", String::from(authorization)));
655 }
656 }
657 }
658
659 if let Ok((resp, resp_stream)) = conn.send(req, |_| {}).await {
660 platform_log(
661 LOG_TAG,
662 format!(
663 "retrieve_chatbot_info_inner resp.status_code = {}",
664 resp.status_code
665 ),
666 );
667
668 if (resp.status_code >= 200 && resp.status_code < 300) || resp.status_code == 304 {
669 if let Some(authentication_info_header) =
670 header::search(&resp.headers, b"Authentication-Info", false)
671 {
672 if let Some(digest_answer) = digest_answer {
673 if let Some(challenge) = &digest_answer.challenge {
674 security_context.update_auth_info(
675 authentication_info_header,
676 host,
677 b"\"",
678 challenge,
679 false,
680 );
681 }
682 }
683 }
684 }
685
686 if resp.status_code == 200 || resp.status_code == 304 {
687 let response_etag =
688 if let Some(etag_header) = header::search(&resp.headers, b"ETag", false) {
689 match String::from_utf8(etag_header.get_value().to_vec()) {
690 Ok(etag) => Some(etag),
691 Err(_) => None,
692 }
693 } else {
694 None
695 };
696
697 let expiry = if let Some(cache_control_header) =
698 header::search(&resp.headers, b"Cache-Control", false)
699 {
700 let cc = cache_control::parse(cache_control_header.get_value());
701 cc.max_age
702 } else {
703 7 * 24 * 60 * 60
704 };
705
706 if resp.status_code == 200 {
707 if let Some(mut resp_stream) = resp_stream {
708 let mut resp_data = Vec::new();
709 match resp_stream.read_to_end(&mut resp_data).await {
710 Ok(size) => {
711 platform_log(
712 LOG_TAG,
713 format!(
714 "retrieve_chatbot_info_inner resp.data read {} bytes",
715 &size
716 ),
717 );
718
719 if let Ok(resp_string) = String::from_utf8(resp_data) {
720 platform_log(
721 LOG_TAG,
722 format!(
723 "retrieve_chatbot_info_inner resp.string = {}",
724 &resp_string
725 ),
726 );
727
728 return Ok(RetrieveChatbotInfoSuccess::Ok(
729 resp_string,
730 response_etag,
731 expiry,
732 ));
733 }
734 }
735
736 Err(e) => {
737 platform_log(LOG_TAG, format!("retrieve_chatbot_info_inner error reading response stream: {:?}", e));
738 }
739 }
740 } else {
741 platform_log(
742 LOG_TAG,
743 "retrieve_chatbot_info_inner missing response body",
744 );
745 }
746 } else {
747 return Ok(RetrieveChatbotInfoSuccess::NotModified(
748 response_etag,
749 expiry,
750 ));
751 }
752 } else if resp.status_code == 401 {
753 if digest_answer.is_none() {
754 if let Some(www_authenticate_header) =
755 header::search(&resp.headers, b"WWW-Authenticate", false)
756 {
757 if let Some(Ok(answer)) = gba::try_process_401_response(
758 gba_context,
759 host.as_bytes(),
760 conn.cipher_id(),
761 GET,
762 b"\"/\"",
763 None,
764 www_authenticate_header,
765 http_client,
766 security_context,
767 )
768 .await
769 {
770 return retrieve_chatbot_info(
771 bot_domain,
772 bot_id,
773 local_etag,
774 home_operator,
775 home_language,
776 msisdn,
777 http_client,
778 gba_context,
779 security_context,
780 Some(&answer),
781 )
782 .await;
783 }
784 }
785 }
786 } else {
787 return Err(RetrieveChatbotInfoError::Http(
788 resp.status_code,
789 match String::from_utf8(resp.reason_phrase) {
790 Ok(reason_phrase) => reason_phrase,
791 Err(_) => String::from(""),
792 },
793 ));
794 }
795 }
796 }
797
798 Err(RetrieveChatbotInfoError::NetworkIO)
799 } else {
800 Err(RetrieveChatbotInfoError::MalformedUrl)
801 }
802}
803
804pub fn retrieve_chatbot_info<'a, 'b: 'a>(
805 bot_domain: &'b str,
806 bot_id: &'b str,
807 local_etag: Option<&'b str>,
808 home_operator: &'b str,
809 home_language: &'b str,
810 msisdn: Option<&'b str>,
811 http_client: &'b Arc<HttpClient>,
812 gba_context: &'b Arc<GbaContext>,
813 security_context: &'b Arc<SecurityContext>,
814 digest_answer: Option<&'a DigestAnswerParams>,
815) -> BoxFuture<'a, Result<RetrieveChatbotInfoSuccess, RetrieveChatbotInfoError>> {
816 async move {
817 retrieve_chatbot_info_inner(
818 bot_domain,
819 bot_id,
820 local_etag,
821 home_operator,
822 home_language,
823 msisdn,
824 http_client,
825 gba_context,
826 security_context,
827 digest_answer,
828 )
829 .await
830 }
831 .boxed()
832}