use std::sync::{Arc, Mutex, Once};
use tokio::sync::{broadcast, mpsc, watch, Notify};
use wreq::{
Client, EmulationProvider, Http2Config, Method, PseudoOrder, SettingsOrder, SslCurve,
TlsConfig, TlsVersion,
};
use crate::auth::{AuthEvent, AuthState, LoginResult, Session};
use crate::device::DeviceInfo;
use crate::error::GrindrError;
use crate::headers::build_user_agent;
use crate::rest::{Fingerprint, InnerClient, RawResponse};
use crate::ws::{make_channels, WsChannels, WsCommand, WsConnectionState, WsEvent};
const MODERN_TLS_CIPHERS: &str = concat!(
"TLS_AES_128_GCM_SHA256",
":TLS_AES_256_GCM_SHA384",
":TLS_CHACHA20_POLY1305_SHA256",
":TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
":TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
":TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
":TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
":TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
":TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
":TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
":TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
":TLS_RSA_WITH_AES_128_GCM_SHA256",
":TLS_RSA_WITH_AES_256_GCM_SHA384",
":TLS_RSA_WITH_AES_128_CBC_SHA",
":TLS_RSA_WITH_AES_256_CBC_SHA",
);
const SIGALGS: &str = concat!(
"ecdsa_secp256r1_sha256",
":rsa_pss_rsae_sha256",
":rsa_pkcs1_sha256",
":ecdsa_secp384r1_sha384",
":rsa_pss_rsae_sha384",
":rsa_pkcs1_sha384",
":rsa_pss_rsae_sha512",
":rsa_pkcs1_sha512",
":rsa_pkcs1_sha1",
);
const CURVES: &[SslCurve] = &[SslCurve::X25519, SslCurve::SECP256R1, SslCurve::SECP384R1];
const PSEUDO_ORDER: [PseudoOrder; 4] = [
PseudoOrder::Method,
PseudoOrder::Path,
PseudoOrder::Authority,
PseudoOrder::Scheme,
];
const SETTINGS_ORDER: [SettingsOrder; 8] = [
SettingsOrder::InitialWindowSize,
SettingsOrder::HeaderTableSize,
SettingsOrder::EnablePush,
SettingsOrder::MaxConcurrentStreams,
SettingsOrder::MaxFrameSize,
SettingsOrder::MaxHeaderListSize,
SettingsOrder::UnknownSetting8,
SettingsOrder::UnknownSetting9,
];
const OKHTTP_WINDOW_SIZE: u32 = 16 * 1024 * 1024;
fn okhttp_tls_config() -> TlsConfig {
TlsConfig::builder()
.enable_ocsp_stapling(true)
.pre_shared_key(true)
.curves(CURVES)
.sigalgs_list(SIGALGS)
.cipher_list(MODERN_TLS_CIPHERS)
.min_tls_version(TlsVersion::TLS_1_2)
.max_tls_version(TlsVersion::TLS_1_3)
.build()
}
fn okhttp_http2_config() -> Http2Config {
Http2Config::builder()
.initial_stream_window_size(OKHTTP_WINDOW_SIZE)
.initial_connection_window_size(OKHTTP_WINDOW_SIZE)
.headers_pseudo_order(PSEUDO_ORDER)
.settings_order(SETTINGS_ORDER)
.build()
}
fn grindr_emulation() -> EmulationProvider {
EmulationProvider::builder()
.tls_config(okhttp_tls_config())
.http2_config(okhttp_http2_config())
.default_headers(None)
.build()
}
pub fn probe_emulation() -> EmulationProvider {
grindr_emulation()
}
fn grindr_client_builder() -> wreq::ClientBuilder {
Client::builder()
.emulation(grindr_emulation())
.gzip(true)
.no_deflate()
.no_brotli()
.no_zstd()
}
fn build_http_client() -> Result<Client, GrindrError> {
grindr_client_builder().build().map_err(Into::into)
}
fn build_ws_client() -> Result<Client, GrindrError> {
grindr_client_builder()
.http1_only()
.build()
.map_err(Into::into)
}
fn build_fingerprint(device: DeviceInfo) -> Result<Arc<Fingerprint>, GrindrError> {
let user_agent = build_user_agent(&device, "Free");
let http = build_http_client()?;
let ws_http = build_ws_client()?;
Ok(Arc::new(Fingerprint {
http,
ws_http,
device,
user_agent,
}))
}
struct WsSpawn {
inner: Arc<InnerClient>,
auth: Arc<AuthState>,
channels: WsChannels,
cmd_rx: mpsc::Receiver<WsCommand>,
logout_notify: Arc<Notify>,
}
#[derive(Clone)]
pub struct GrindrClient {
inner: Arc<InnerClient>,
auth: Arc<AuthState>,
session_rx: watch::Receiver<Option<Session>>,
ws_event_tx: broadcast::Sender<WsEvent>,
ws_cmd_tx: mpsc::Sender<WsCommand>,
ws_state_rx: watch::Receiver<WsConnectionState>,
logout_notify: Arc<Notify>,
ws_started: Arc<Once>,
ws_spawn: Arc<Mutex<Option<WsSpawn>>>,
}
impl GrindrClient {
pub fn new(device: DeviceInfo, session: Option<Session>) -> Result<Self, GrindrError> {
let fingerprint = build_fingerprint(device)?;
let inner = Arc::new(InnerClient {
fingerprint: tokio::sync::RwLock::new(fingerprint),
});
let (auth_state, session_rx) = AuthState::new(session);
let auth = Arc::new(auth_state);
let (ws_channels, ws_handles) = make_channels();
let logout_notify = Arc::new(Notify::new());
let ws_event_tx = ws_channels.event_tx.clone();
let ws_cmd_tx = ws_handles.cmd_tx;
let ws_state_rx = ws_handles.state_rx;
let ws_spawn = WsSpawn {
inner: Arc::clone(&inner),
auth: Arc::clone(&auth),
channels: ws_channels,
cmd_rx: ws_handles.cmd_rx,
logout_notify: Arc::clone(&logout_notify),
};
Ok(Self {
inner,
auth,
session_rx,
ws_event_tx,
ws_cmd_tx,
ws_state_rx,
logout_notify,
ws_started: Arc::new(Once::new()),
ws_spawn: Arc::new(Mutex::new(Some(ws_spawn))),
})
}
fn ensure_ws_task(&self) {
self.ws_started.call_once(|| {
if let Some(parts) = self.ws_spawn.lock().unwrap().take() {
crate::ws::spawn_ws_task(
parts.inner,
parts.auth,
parts.channels,
parts.cmd_rx,
parts.logout_notify,
);
}
});
}
pub fn auth_event_receiver(&self) -> broadcast::Receiver<AuthEvent> {
self.auth.auth_event_tx.subscribe()
}
pub fn session_receiver(&self) -> watch::Receiver<Option<Session>> {
self.session_rx.clone()
}
pub fn connection_state(&self) -> watch::Receiver<WsConnectionState> {
self.ws_state_rx.clone()
}
pub fn ws_receiver(&self) -> broadcast::Receiver<WsEvent> {
self.ws_event_tx.subscribe()
}
pub fn ws_sender(&self) -> mpsc::Sender<WsCommand> {
self.ws_cmd_tx.clone()
}
pub async fn connect(&self) {
self.ensure_ws_task();
}
pub async fn login(&self, email: &str, password: &str) -> Result<LoginResult, GrindrError> {
self.ensure_ws_task();
crate::auth::login_email(&self.inner, &self.auth, email, password).await
}
pub async fn google_sign_in(
&self,
google_access_token: &str,
) -> Result<LoginResult, GrindrError> {
self.ensure_ws_task();
crate::auth::google_sign_in(&self.inner, &self.auth, google_access_token).await
}
pub async fn refresh_token(&self) -> Result<LoginResult, GrindrError> {
self.ensure_ws_task();
crate::auth::refresh_token(&self.inner, &self.auth).await
}
pub async fn logout(&self) {
self.auth.clear_session().await;
self.logout_notify.notify_waiters();
}
pub async fn request_authenticated_raw(
&self,
method: Method,
path: &str,
body: Option<serde_json::Value>,
) -> Result<RawResponse, GrindrError> {
self.ensure_ws_task();
self.inner
.request_authenticated_raw(&self.auth, method, path, body)
.await
}
pub async fn rotate_device(&self, device: DeviceInfo) -> Result<DeviceInfo, GrindrError> {
let new_fp = build_fingerprint(device)?;
let old_fp = {
let mut guard = self.inner.fingerprint.write().await;
std::mem::replace(&mut *guard, new_fp)
};
Ok(old_fp.device.clone())
}
pub async fn current_device(&self) -> DeviceInfo {
self.inner.fingerprint().await.device.clone()
}
pub async fn recaptcha_first_party_enabled(&self) -> Result<bool, GrindrError> {
crate::auth::recaptcha_first_party_enabled(&self.inner).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_does_not_require_a_runtime() {
let client = GrindrClient::new(DeviceInfo::generate(), None).unwrap();
assert!(!client.ws_started.is_completed());
}
#[tokio::test]
async fn ws_task_spawns_on_first_async_call() {
let client = GrindrClient::new(DeviceInfo::generate(), None).unwrap();
let err = client
.request_authenticated_raw(Method::GET, "/v3/me/profile", None)
.await
.unwrap_err();
assert!(matches!(err, GrindrError::Auth(_)));
assert!(client.ws_started.is_completed());
}
#[tokio::test]
async fn connect_starts_the_ws_task() {
let client = GrindrClient::new(DeviceInfo::generate(), None).unwrap();
assert!(!client.ws_started.is_completed());
client.connect().await;
assert!(client.ws_started.is_completed());
}
#[tokio::test]
async fn dropping_client_in_async_context_does_not_panic() {
let client = GrindrClient::new(DeviceInfo::generate(), None).unwrap();
let clone = client.clone();
drop(client);
drop(clone);
}
}