1use std::{
2 borrow::Borrow,
3 fmt::Debug,
4 sync::{atomic::AtomicU32, Arc},
5 time::Duration,
6};
7
8use log::{error, trace, warn};
9use reqwest::{header::*, Client};
10use url::Url;
11
12use crate::{
13 command::Command,
14 error::SFError,
15 gamestate::{
16 character::{Class, Gender, Race},
17 GameState,
18 },
19 misc::{
20 encrypt_server_request, sha1_hash, DEFAULT_CRYPTO_ID,
21 DEFAULT_CRYPTO_KEY, DEFAULT_SESSION_ID, HASH_CONST,
22 },
23};
24pub use crate::{misc::decrypt_url, response::*};
25
26#[derive(Debug, Clone)]
27pub struct Session {
29 login_data: LoginData,
31 server_url: url::Url,
33 session_id: String,
36 command_count: Arc<AtomicU32>,
38 login_count: u32,
39 crypto_id: String,
40 crypto_key: String,
41 client: reqwest::Client,
45 options: ConnectionOptions,
46}
47
48#[derive(Debug, Clone)]
49#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
50pub struct PWHash(String);
52
53impl PWHash {
54 #[must_use]
57 pub fn new(password: &str) -> Self {
58 Self(sha1_hash(&(password.to_string() + HASH_CONST)))
59 }
60 #[must_use]
63 pub fn from_hash(hash: String) -> Self {
64 Self(hash)
65 }
66
67 #[must_use]
69 pub fn get(&self) -> &str {
70 &self.0
71 }
72}
73
74impl Session {
75 #[must_use]
80 pub fn new(
81 username: &str,
82 password: &str,
83 server: ServerConnection,
84 ) -> Self {
85 Self::new_hashed(username, PWHash::new(password), server)
86 }
87
88 #[must_use]
90 pub fn new_hashed(
91 username: &str,
92 pw_hash: PWHash,
93 server: ServerConnection,
94 ) -> Self {
95 let ld = LoginData::Basic {
96 username: username.to_string(),
97 pw_hash,
98 };
99 Self::new_full(ld, server.client, server.options, server.url)
100 }
101
102 fn new_full(
103 ld: LoginData,
104 client: Client,
105 options: ConnectionOptions,
106 url: Url,
107 ) -> Self {
108 Self {
109 login_data: ld,
110 server_url: url,
111 client,
112 session_id: DEFAULT_SESSION_ID.to_string(),
113 crypto_id: DEFAULT_CRYPTO_ID.to_string(),
114 crypto_key: DEFAULT_CRYPTO_KEY.to_string(),
115 command_count: Arc::new(AtomicU32::new(0)),
116 login_count: 1,
117 options,
118 }
119 }
120
121 fn logout(&mut self) {
125 self.crypto_key = DEFAULT_CRYPTO_KEY.to_string();
126 self.crypto_id = DEFAULT_CRYPTO_ID.to_string();
127 self.login_count = 1;
128 self.command_count = Arc::new(AtomicU32::new(0));
129 self.session_id = DEFAULT_SESSION_ID.to_string();
130 }
131
132 #[must_use]
135 pub fn server_url(&self) -> &url::Url {
136 &self.server_url
137 }
138
139 #[must_use]
144 pub fn has_session_id(&self) -> bool {
145 self.session_id.chars().any(|a| a != '0')
146 }
147
148 pub async fn login(&mut self) -> Result<Response, SFError> {
156 self.logout();
157 #[allow(deprecated)]
158 let login_cmd = match self.login_data.clone() {
159 LoginData::Basic { username, pw_hash } => Command::Login {
160 username,
161 pw_hash: pw_hash.get().to_string(),
162 login_count: self.login_count,
163 },
164 #[cfg(feature = "sso")]
165 LoginData::SSO {
166 character_id,
167 session,
168 ..
169 } => Command::SSOLogin {
170 uuid: session.uuid,
171 character_id,
172 bearer_token: session.bearer_token,
173 },
174 };
175
176 self.send_command(&login_cmd).await
177 }
178
179 pub async fn register(
186 username: &str,
187 password: &str,
188 server: ServerConnection,
189 gender: Gender,
190 race: Race,
191 class: Class,
192 ) -> Result<(Self, Response), SFError> {
193 let mut s = Self::new(username, password, server);
194 #[allow(deprecated)]
195 let resp = s
196 .send_command(&Command::Register {
197 username: username.to_string(),
198 password: password.to_string(),
199 gender,
200 race,
201 class,
202 })
203 .await?;
204
205 let Some(tracking) = resp.values().get("tracking") else {
206 error!("Got no tracking response from server after registering");
207 return Err(SFError::ParsingError(
208 "register response",
209 resp.raw_response().to_string(),
210 ));
211 };
212
213 if tracking.as_str() != "signup" {
214 error!("Got something else than signup response during register");
215 return Err(SFError::ParsingError(
216 "register tracking response",
217 tracking.as_str().to_string(),
218 ));
219 }
220
221 let resp = s.login().await?;
224 Ok((s, resp))
225 }
226
227 pub async fn send_command_raw<T: Borrow<Command>>(
240 &self,
241 command: T,
242 ) -> Result<Response, SFError> {
243 let command = command.borrow();
244 trace!("Sending a {command:?} command");
245
246 let mut command_str =
247 format!("{}|{}", self.session_id, command.request_string()?);
248
249 while command_str.len() % 16 > 0 {
250 command_str.push('|');
251 }
252
253 trace!("Command string: {command_str}");
254 let url = format!(
255 "{}req.php?req={}{}&rnd={:.7}&c={}",
256 self.server_url,
257 &self.crypto_id,
258 encrypt_server_request(command_str, &self.crypto_key)?,
259 fastrand::f64(), self.command_count
261 .fetch_add(1, std::sync::atomic::Ordering::SeqCst)
262 );
263 trace!("Full request url: {url}");
264
265 url::Url::parse(&url).map_err(|_| {
267 SFError::InvalidRequest("Could not parse command url")
268 })?;
269
270 #[allow(unused_mut)]
271 let mut req = self
272 .client
273 .get(&url)
274 .header(REFERER, &self.server_url.to_string());
275
276 #[cfg(feature = "sso")]
277 if let LoginData::SSO { session, .. } = &self.login_data {
278 req = req.bearer_auth(&session.bearer_token);
279 }
280
281 let resp = req.send().await.map_err(|_| SFError::ConnectionError)?;
282
283 if !resp.status().is_success() {
284 return Err(SFError::ConnectionError);
285 }
286
287 let response_body =
288 resp.text().await.map_err(|_| SFError::ConnectionError)?;
289
290 match response_body {
291 body if body.is_empty() => Err(SFError::EmptyResponse),
292 body => {
293 let resp =
294 Response::parse(body, chrono::Local::now().naive_local())?;
295 if let Some(lc) = resp.values().get("serverversion").copied() {
296 let version: u32 = lc.into("server version")?;
297 if version > self.options.expected_server_version {
298 warn!("Untested S&F Server version: {version}");
299 if self.options.error_on_unsupported_version {
300 return Err(SFError::UnsupportedVersion(version));
301 }
302 }
303 }
304 Ok(resp)
305 }
306 }
307 }
308
309 pub async fn send_command<T: Borrow<Command>>(
326 &mut self,
327 command: T,
328 ) -> Result<Response, SFError> {
329 let res = self.send_command_raw(command).await?;
330 self.update(&res);
331 Ok(res)
332 }
333
334 pub fn update(&mut self, res: &Response) {
337 let data = res.values();
338 if let Some(lc) = data.get("login count") {
339 self.login_count = (*lc).into("login count").unwrap_or_default();
340 }
341 if let Some(lc) = data.get("sessionid") {
342 self.session_id.clear();
343 self.session_id.push_str(lc.as_str());
344 }
345 if let Some(lc) = data.get("cryptokey") {
346 self.crypto_key.clear();
347 self.crypto_key.push_str(lc.as_str());
348 }
349 if let Some(lc) = data.get("cryptoid") {
350 self.crypto_id.clear();
351 self.crypto_id.push_str(lc.as_str());
352 }
353 }
354
355 #[cfg(feature = "sso")]
356 pub(super) async fn from_sso_char(
357 character: crate::sso::SSOCharacter,
358 account: std::sync::Arc<tokio::sync::Mutex<crate::sso::SFAccount>>,
359 server_lookup: &crate::sso::ServerLookup,
360 ) -> Result<Session, SFError> {
361 let url = server_lookup.get(character.server_id)?;
362 let session = account.lock().await.session.clone();
363 let client = account.lock().await.client.clone();
364 let options = account.lock().await.options.clone();
365
366 let ld = LoginData::SSO {
367 username: character.name,
368 character_id: character.id,
369 account,
370 session,
371 };
372 Ok(Session::new_full(ld, client, options, url))
373 }
374
375 #[must_use]
376 pub fn username(&self) -> &str {
378 match &self.login_data {
379 LoginData::Basic { username, .. } => username,
380 #[cfg(feature = "sso")]
381 LoginData::SSO {
382 username: character_name,
383 ..
384 } => character_name,
385 }
386 }
387
388 #[cfg(feature = "sso")]
389 pub async fn renew_sso_creds(&mut self) -> Result<(), SFError> {
399 let LoginData::SSO {
400 account, session, ..
401 } = &mut self.login_data
402 else {
403 return Err(SFError::InvalidRequest(
404 "Can not renow sso credentials for a non-sso account",
405 ));
406 };
407 let mut account = account.lock().await;
408
409 if &account.session == session {
410 account.refresh_login().await?;
411 } else {
412 *session = account.session.clone();
413 }
414 Ok(())
415 }
416}
417
418#[derive(Debug, Clone)]
419#[allow(clippy::upper_case_acronyms)]
420#[non_exhaustive]
421enum LoginData {
422 Basic {
423 username: String,
424 pw_hash: PWHash,
425 },
426 #[cfg(feature = "sso")]
427 SSO {
428 username: String,
429 character_id: String,
430 account: std::sync::Arc<tokio::sync::Mutex<crate::sso::SFAccount>>,
433 session: crate::sso::AccountSession,
444 },
445}
446
447#[derive(Debug, Clone)]
448pub struct ServerConnection {
453 url: url::Url,
454 client: Client,
455 options: ConnectionOptions,
456}
457
458impl ServerConnection {
459 #[must_use]
462 pub fn new(server_url: &str) -> Option<ServerConnection> {
463 ServerConnection::new_with_options(
464 server_url,
465 ConnectionOptions::default(),
466 )
467 }
468
469 #[must_use]
473 pub fn new_with_options(
474 server_url: &str,
475 options: ConnectionOptions,
476 ) -> Option<ServerConnection> {
477 let url = if server_url.starts_with("http") {
478 server_url.parse().ok()?
479 } else {
480 format!("https://{server_url}").parse().ok()?
481 };
482
483 Some(ServerConnection {
484 url,
485 client: reqwest_client(&options)?,
486 options,
487 })
488 }
489}
490
491pub(crate) fn reqwest_client(
492 options: &ConnectionOptions,
493) -> Option<reqwest::Client> {
494 let mut headers = HeaderMap::new();
495 headers.insert(
496 HeaderName::from_static(ACCEPT_LANGUAGE.as_str()),
497 HeaderValue::from_static("en;q=0.7,en-US;q=0.6"),
498 );
499 let mut builder = reqwest::Client::builder();
500 if let Some(ua) = options.user_agent.clone() {
501 builder = builder.user_agent(ua);
502 }
503 builder.default_headers(headers).build().ok()
504}
505
506#[derive(Debug, Clone)]
507pub struct ConnectionOptions {
509 pub user_agent: Option<String>,
511 pub expected_server_version: u32,
513 pub error_on_unsupported_version: bool,
518}
519
520impl Default for ConnectionOptions {
521 fn default() -> Self {
522 Self {
523 user_agent: Some(
524 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \
525 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"
526 .to_string(),
527 ),
528 expected_server_version: 2005,
529 error_on_unsupported_version: false,
530 }
531 }
532}
533
534#[derive(Debug)]
535#[allow(clippy::module_name_repetitions)]
536pub struct SimpleSession {
537 session: Session,
538 gamestate: Option<GameState>,
539}
540
541impl SimpleSession {
542 async fn short_sleep() {
543 tokio::time::sleep(Duration::from_millis(fastrand::u64(1000..2000)))
544 .await;
545 }
546
547 pub async fn login(
552 username: &str,
553 password: &str,
554 server_url: &str,
555 ) -> Result<Self, SFError> {
556 let connection = ServerConnection::new(server_url)
557 .ok_or(SFError::ConnectionError)?;
558 let mut session = Session::new(username, password, connection);
559 let resp = session.login().await?;
560 let gs = GameState::new(resp)?;
561 Self::short_sleep().await;
562 Ok(Self {
563 session,
564 gamestate: Some(gs),
565 })
566 }
567
568 #[cfg(feature = "sso")]
569 pub async fn login_sf_account(
575 username: &str,
576 password: &str,
577 ) -> Result<Vec<Self>, SFError> {
578 let acc = crate::sso::SFAccount::login(
579 username.to_string(),
580 password.to_string(),
581 )
582 .await?;
583
584 Ok(acc
585 .characters()
586 .await?
587 .into_iter()
588 .flatten()
589 .map(|a| Self {
590 session: a,
591 gamestate: None,
592 })
593 .collect())
594 }
595
596 #[must_use]
599 pub fn game_state(&self) -> Option<&GameState> {
600 self.gamestate.as_ref()
601 }
602
603 #[must_use]
606 pub fn game_state_mut(&mut self) -> Option<&mut GameState> {
607 self.gamestate.as_mut()
608 }
609
610 #[allow(clippy::unwrap_used, clippy::missing_panics_doc)]
629 pub async fn send_command<T: Borrow<Command>>(
630 &mut self,
631 cmd: T,
632 ) -> Result<&mut GameState, SFError> {
633 if self.gamestate.is_none() {
634 let resp = self.session.login().await?;
635 let gs = GameState::new(resp)?;
636 self.gamestate = Some(gs);
637 Self::short_sleep().await;
638 }
639
640 let resp = match self.session.send_command(cmd).await {
641 Ok(resp) => resp,
642 Err(err) => {
643 self.gamestate = None;
644 return Err(err);
645 }
646 };
647
648 if let Some(gs) = &mut self.gamestate {
649 if let Err(e) = gs.update(resp) {
650 self.gamestate = None;
651 return Err(e);
652 }
653 }
654
655 Ok(self.gamestate.as_mut().unwrap())
656 }
657}