1use core::fmt;
2use std::{borrow::Cow, collections::HashMap, str::FromStr};
3
4use crate::utils::BASE64_RELAXED;
5use base64::prelude::*;
6use libsignal_core::DeviceId;
7use libsignal_protocol::PublicKey;
8use serde::{Deserialize, Serialize};
9use url::Url;
10use zkgroup::ServerPublicParams;
11
12use crate::{
13 envelope::{CIPHER_KEY_SIZE, MAC_KEY_SIZE},
14 push_service::{HttpAuth, DEFAULT_DEVICE_ID},
15};
16
17#[derive(Clone)]
18pub struct ServiceConfiguration {
19 service_url: Url,
20 storage_url: Url,
21 cdn_urls: HashMap<u32, Url>,
22 contact_discovery_url: Url,
23 pub certificate_authority: String,
24 pub unidentified_sender_trust_root: PublicKey,
25 pub zkgroup_server_public_params: ServerPublicParams,
26}
27
28pub type SignalingKey = [u8; CIPHER_KEY_SIZE + MAC_KEY_SIZE];
29
30#[derive(Clone)]
31pub struct ServiceCredentials {
32 pub aci: Option<uuid::Uuid>,
33 pub pni: Option<uuid::Uuid>,
34 pub phonenumber: phonenumber::PhoneNumber,
35 pub password: Option<String>,
36 pub signaling_key: Option<SignalingKey>,
37 pub device_id: Option<DeviceId>,
38}
39
40impl ServiceCredentials {
41 pub fn authorization(&self) -> Option<HttpAuth> {
42 self.password.as_ref().map(|password| HttpAuth {
43 username: self.login(),
44 password: password.clone(),
45 })
46 }
47
48 pub fn e164(&self) -> String {
49 self.phonenumber
50 .format()
51 .mode(phonenumber::Mode::E164)
52 .to_string()
53 }
54
55 pub fn login(&self) -> String {
56 let identifier = {
57 if let Some(uuid) = self.aci.as_ref() {
58 uuid.to_string()
59 } else {
60 self.e164()
61 }
62 };
63
64 match self.device_id {
65 None => identifier,
66 Some(device_id) if device_id == *DEFAULT_DEVICE_ID => identifier,
67 Some(id) => format!("{}.{}", identifier, id),
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
73pub enum SignalServers {
74 Staging,
75 Production,
76}
77
78#[derive(Debug)]
79pub enum Endpoint<'a> {
80 Absolute(Url),
81 Service {
82 path: Cow<'a, str>,
83 },
84 Storage {
85 path: Cow<'a, str>,
86 },
87 Cdn {
88 cdn_id: u32,
89 path: Cow<'a, str>,
90 query: Option<Cow<'a, str>>,
91 },
92 ContactDiscovery {
93 path: Cow<'a, str>,
94 },
95}
96
97impl fmt::Display for Endpoint<'_> {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 match self {
100 Endpoint::Absolute(url) => write!(f, "absolute URL {url}"),
101 Endpoint::Service { path } => {
102 write!(f, "service API call to {path}")
103 },
104 Endpoint::Storage { path } => {
105 write!(f, "storage API call to {path}")
106 },
107 Endpoint::Cdn { cdn_id, path, .. } => {
108 write!(f, "CDN{cdn_id} call to {path}")
109 },
110 Endpoint::ContactDiscovery { path } => {
111 write!(f, "Contact discovery API call to {path}")
112 },
113 }
114 }
115}
116
117impl<'a> Endpoint<'a> {
118 pub fn service(path: impl Into<Cow<'a, str>>) -> Self {
119 Self::Service { path: path.into() }
120 }
121
122 pub fn cdn(cdn_id: u32, path: impl Into<Cow<'a, str>>) -> Self {
123 Self::Cdn {
124 cdn_id,
125 path: path.into(),
126 query: None,
127 }
128 }
129
130 pub fn cdn_url(cdn_id: u32, url: &'a Url) -> Self {
131 Self::Cdn {
132 cdn_id,
133 path: url.path().into(),
134 query: url.query().map(Into::into),
135 }
136 }
137
138 pub fn storage(path: impl Into<Cow<'a, str>>) -> Self {
139 Self::Storage { path: path.into() }
140 }
141
142 pub fn into_url(
143 self,
144 service_configuration: &ServiceConfiguration,
145 ) -> Result<Url, url::ParseError> {
146 match self {
147 Endpoint::Service { path } => {
148 service_configuration.service_url.join(&path)
149 },
150 Endpoint::Storage { path } => {
151 service_configuration.storage_url.join(&path)
152 },
153 Endpoint::Cdn {
154 ref cdn_id,
155 path,
156 query,
157 } => {
158 let mut url = service_configuration.cdn_urls[cdn_id].clone();
159 url.set_path(&path);
160 url.set_query(query.as_deref());
161 Ok(url)
162 },
163 Endpoint::ContactDiscovery { path } => {
164 service_configuration.contact_discovery_url.join(&path)
165 },
166 Endpoint::Absolute(url) => Ok(url),
167 }
168 }
169}
170
171impl FromStr for SignalServers {
172 type Err = std::io::Error;
173
174 fn from_str(s: &str) -> Result<Self, Self::Err> {
175 use std::io::ErrorKind;
176 match s {
177 "staging" => Ok(Self::Staging),
178 "production" => Ok(Self::Production),
179 _ => Err(Self::Err::new(
180 ErrorKind::InvalidInput,
181 "invalid signal servers, can be either: staging or production",
182 )),
183 }
184 }
185}
186
187impl fmt::Display for SignalServers {
188 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189 match self {
190 Self::Staging => f.write_str("staging"),
191 Self::Production => f.write_str("production"),
192 }
193 }
194}
195
196impl From<SignalServers> for ServiceConfiguration {
197 fn from(val: SignalServers) -> Self {
198 ServiceConfiguration::from(&val)
199 }
200}
201
202impl From<&SignalServers> for ServiceConfiguration {
203 fn from(val: &SignalServers) -> Self {
204 match val {
206 SignalServers::Staging => ServiceConfiguration {
209 service_url: "https://chat.staging.signal.org".parse().unwrap(),
210 storage_url:"https://storage-staging.signal.org".parse().unwrap(),
211 cdn_urls: {
212 let mut map = HashMap::new();
213 map.insert(0, "https://cdn-staging.signal.org".parse().unwrap());
214 map.insert(2, "https://cdn2-staging.signal.org".parse().unwrap());
215 map.insert(3, "https://cdn3-staging.signal.org".parse().unwrap());
216 map
217 },
218 contact_discovery_url:
219 "https://api-staging.directory.signal.org".parse().unwrap(),
220 certificate_authority: include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/certs/staging-root-ca.pem")).to_string(),
221 unidentified_sender_trust_root:
222 PublicKey::deserialize(&BASE64_RELAXED.decode("BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx").unwrap()).unwrap(),
223 zkgroup_server_public_params: bincode::deserialize(&BASE64_RELAXED.decode("ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCNWI+wwkyFCqNDXz/qxl1gAntuCJtSfq9OC3NkdhQlgYQ==").unwrap()).unwrap(),
224 },
225 SignalServers::Production => ServiceConfiguration {
228 service_url:
229 "https://chat.signal.org".parse().unwrap(),
230 storage_url: "https://storage.signal.org".parse().unwrap(),
231 cdn_urls: {
232 let mut map = HashMap::new();
233 map.insert(0, "https://cdn.signal.org".parse().unwrap());
234 map.insert(2, "https://cdn2.signal.org".parse().unwrap());
235 map.insert(3, "https://cdn3.signal.org".parse().unwrap());
236 map
237 },
238 contact_discovery_url: "https://api.directory.signal.org".parse().unwrap(),
239 certificate_authority: include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/certs/production-root-ca.pem")).to_string(),
240 unidentified_sender_trust_root:
241 PublicKey::deserialize(&BASE64_RELAXED.decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF").unwrap()).unwrap(),
242 zkgroup_server_public_params: bincode::deserialize(
243 &BASE64_RELAXED.decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0LUlT9vALgh/f2DPVOOmR0RW6bgRvc7DSF20V/omg+YBw==").unwrap()).unwrap(),
244 },
245 }
246 }
247}