pub mod consts;
use reqwest::blocking::get;
use reqwest::blocking::Client;
use reqwest::blocking::ClientBuilder;
use reqwest::header;
use serde::Deserialize;
use serde::Serialize;
use serde_json::from_str;
use std::collections::HashMap;
use std::env::var;
use std::fs::read_to_string;
use std::fs::remove_file;
use std::fs::OpenOptions;
use std::io::Write;
use std::ops::Deref;
use std::ops::DerefMut;
use std::ops::Index;
use std::path::Path;
use std::path::PathBuf;
use std::process::Child as _Child;
use std::process::Command;
use std::process::Stdio;
use url::Url;
#[derive(Debug)]
pub struct PlayIt {
pub req_client: Client,
pub os: OS,
pub config_file: PathBuf,
pub arch: Arch,
pub dir: PathBuf,
pub destroyed: bool,
pub tunnels: Vec<Tunnel>,
pub agent_key: String,
pub started: bool,
pub playit: Child,
pub server: String,
pub used_packets: u8,
pub free_packets: u8,
pub connections: Vec<Connection>,
pub version: String,
pub download_urls: Binaries,
pub binary: PathBuf,
pub binary_type: BinaryType,
pub output: Vec<String>,
pub stdout: Vec<String>,
pub stderr: Vec<String>,
pub errors: Vec<String>,
pub warnings: Vec<String>,
pub api_path: Url,
}
impl PlayIt {
pub fn new() -> Self {
Self::default()
}
pub fn create_tunnel(
&self,
port: u16,
proto: Prototype,
ip: Option<&str>,
) -> Result<Tunnel, reqwest::Error> {
use serde_json::json;
use serde_json::to_string;
let ip = ip.unwrap_or("127.0.0.1");
let tunnel_id = self
.req_client
.post(self.api_path.join("/account/tunnels").unwrap())
.body(
to_string(&json!({
"id": null,
"game": format!("custom-{}", match proto {
Prototype::Tcp => "tcp",
Prototype::Udp => "udp",
Prototype::Both => "both"
}),
"local_port": port,
"local_ip": ip,
"local_proto": proto,
"agent_id": &self
.req_client
.get(self.api_path.join("/account/agents").unwrap())
.send()
.unwrap()
.json::<Agents>()
.unwrap()
.agents
.into_iter()
.find(|agent| agent.key == self.agent_key)
.unwrap()
.id,
"domain_id": null
}))
.unwrap(),
)
.send()?
.json::<Id>()?
.id;
loop {
let data = self
.req_client
.get(self.api_path.join("/account/tunnels").unwrap())
.send()
.unwrap()
.json::<Tunnels>()
.unwrap()
.tunnels
.into_iter()
.find(|tunnel| tunnel.id == tunnel_id)
.unwrap();
if data.domain_id.is_some() && data.connect_address.is_some() {
break Ok(data);
}
}
}
#[cfg(feature = "enable-tunnels")]
pub fn enable_tunnel(&self, id: i32) -> Result<(), reqwest::Error> {
self
.req_client
.get(
self
.api_path
.join(&format!("/account/tunnels/{}/enable", id))
.unwrap(),
)
.send()?;
Ok(())
}
#[cfg(feature = "disable-tunnels")]
pub fn disable_tunnel(&self, id: i32) -> Result<(), reqwest::Error> {
self
.req_client
.get(
self
.api_path
.join(&format!("/account/tunnels/{}/disable", id))
.unwrap(),
)
.send()?;
Ok(())
}
}
impl Default for PlayIt {
fn default() -> Self {
let dir = Path::new(&if consts::OS == OS::Linux {
if var("XDG_CONFIG_HOME").is_err() {
format!("{}/.config/playit/", var("HOME").unwrap())
} else {
format!("{}/playit/", var("XDG_CONFIG_HOME").unwrap())
}
} else if consts::OS == OS::Windows {
format!(r"{}\playit\", var("AppData").unwrap())
} else {
format!(
"{}/Library/Application Support/playit/",
var("HOME").unwrap()
)
})
.to_owned();
let config_file = {
let path = dir.join("config.json");
remove_file(&path).ok();
path
};
let connections = Vec::new();
let download_urls = Binaries {
win: Url::parse(&format!(
"https://playit.gg/downloads/playit-win_64-{}.exe",
consts::VERSION
))
.expect("Failed To Parse Download URL"),
lin: Url::parse(&format!(
"https://playit.gg/downloads/playit-linux_64-{}",
consts::VERSION
))
.expect("Failed To Parse Download URL"),
mac: Url::parse(&format!(
"https://playit.gg/downloads/playit-darwin_64-{}.zip",
consts::VERSION
))
.expect("Failed To Parse Download URL"),
arm64: Url::parse(&format!(
"https://playit.gg/downloads/playit-aarch64-{}",
consts::VERSION
))
.expect("Failed To Parse Download URL"),
arm: Url::parse(&format!(
"https://playit.gg/downloads/playit-armv7-{}",
consts::VERSION
))
.expect("Failed To Parse Download URL"),
};
let binary = (|| {
let file = dir.join(&format!(
"playit-{:#?}-{}.{}",
consts::BINARY_TYPE,
consts::VERSION,
match consts::OS {
OS::Windows => "exe",
OS::MacOS => "zip",
_ => "bin",
}
));
if file.exists() {
return file;
}
let mut _file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(true)
.open(&file)
.unwrap();
_file
.write_all(
&get(download_urls[consts::BINARY_TYPE].clone())
.unwrap()
.bytes()
.unwrap(),
)
.expect("Failed To Download PlayIt");
#[cfg(target_os = "macos")]
{
use std::fs::copy;
use std::fs::remove_dir_all;
use zip::ZipArchive;
let mut zip = ZipArchive::new(_file).unwrap();
zip.extract(dir).unwrap();
let file = dir.join(format!(
"playit-{:#?}-{}.bin",
BinaryType::MacOS,
consts::VERSION,
));
copy(
dir.join(format!(
"playit-darwin_64-{}/playit-darwin_64-{}",
consts::VERSION,
consts::VERSION
)),
&file,
)
.unwrap();
remove_dir_all(dir.join(format!("playit-darwin_64-{}", consts::VERSION))).unwrap();
}
#[cfg(any(
target_os = "linux",
target_os = "freebsd",
target_os = "dragonfly",
target_os = "openbsd",
target_os = "netbsd"
))]
{
use std::fs::set_permissions;
use std::fs::Permissions;
use std::os::unix::fs::PermissionsExt;
set_permissions(&file, Permissions::from_mode(0o777)).unwrap();
}
file
})();
let playit = {
let mut envs = HashMap::new();
envs.insert("NO_BROWSER", "true".to_owned());
let mut command = Command::new(binary.to_str().unwrap());
command
.envs(envs)
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.stdout(Stdio::piped());
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
command.creation_flags(0x08000000);
}
let command = Child(command.spawn().expect("Failed To Execute PlayIt"));
command
};
let (agent_key, server) = loop {
let config =
from_str::<Config>(&read_to_string(&config_file).unwrap_or_else(|_| "{}".to_owned()))
.unwrap_or(Config {
agent_key: None,
preferred_tunnel: None,
});
if config.agent_key.is_some() && config.preferred_tunnel.is_some() {
break (config.agent_key.unwrap(), config.preferred_tunnel.unwrap());
}
};
Self {
req_client: {
let mut headers = header::HeaderMap::new();
let mut auth = header::HeaderValue::from_str(&format!("agent {}", agent_key)).unwrap();
auth.set_sensitive(true);
headers.insert(header::AUTHORIZATION, auth);
ClientBuilder::new()
.default_headers(headers)
.build()
.unwrap()
},
os: consts::OS,
config_file,
arch: consts::ARCH,
dir,
destroyed: false,
tunnels: Vec::new(),
agent_key,
started: false,
playit,
server,
used_packets: 0,
free_packets: 0,
connections,
version: consts::VERSION.to_owned(),
download_urls,
binary,
binary_type: consts::BINARY_TYPE,
output: Vec::new(),
stdout: Vec::new(),
stderr: Vec::new(),
errors: Vec::new(),
warnings: Vec::new(),
api_path: Url::parse("https://api.playit.gg/").unwrap(),
}
}
}
#[allow(non_snake_case)]
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
pub struct PlayItOpts {
pub PREFERRED_TUNNEL: Option<String>,
pub PREFERRED_THRESHOLD: Option<u16>,
pub NO_SPECIAL_LAN: Option<bool>,
pub KEEP_CONFIG: Option<bool>,
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
pub struct Tunnel {
pub id: u32,
pub agent_id: u32,
pub game: String,
pub local_ip: String,
pub local_port: u16,
pub domain_id: Option<u32>,
pub status: String,
pub connect_address: Option<String>,
pub is_custom_domain: bool,
pub tunnel_version: Option<u8>,
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
pub struct Tunnels {
pub tunnels: Vec<Tunnel>,
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
pub struct Connection {
pub ip: String,
pub tunnel: Tunnel,
pub proto: Prototype,
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
pub struct Agent {
pub tunnel_server_name: Option<String>,
pub host_ip: Option<String>,
pub is_connected: bool,
pub key: String,
pub id: u32,
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
pub struct Agents {
pub agents: Vec<Agent>,
}
#[derive(Clone, PartialEq, Debug)]
pub struct Binaries {
pub win: Url,
pub lin: Url,
pub mac: Url,
pub arm64: Url,
pub arm: Url,
}
impl Index<BinaryType> for Binaries {
type Output = Url;
fn index(&self, binary_type: BinaryType) -> &Self::Output {
match binary_type {
BinaryType::Windows => &self.win,
BinaryType::Linux => &self.lin,
BinaryType::MacOS => &self.mac,
BinaryType::Arm64 => &self.arm64,
BinaryType::Arm => &self.arm,
}
}
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
pub struct Config {
pub agent_key: Option<String>,
pub preferred_tunnel: Option<String>,
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
pub struct Id {
pub id: u32,
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
pub enum Prototype {
Tcp,
Udp,
Both,
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
pub enum OS {
Windows,
Linux,
MacOS,
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
pub enum Arch {
X64,
Arm64,
Arm,
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
pub enum BinaryType {
Windows,
Linux,
MacOS,
Arm64,
Arm,
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
pub enum Servers {
Dal4,
Sol4,
Syd4,
Mum4,
Sf4,
Fnk4,
Bng4,
Sng4,
Tor4,
Ny4,
Uk4,
Saw4,
Turk4,
San4,
Pet4,
Bur4,
New4,
Isr4,
Tko4,
Syd5,
Sng5,
Hel4,
Fal4,
}
#[derive(Debug)]
pub struct Child(pub _Child);
impl Drop for Child {
fn drop(&mut self) {
self.0.kill().ok();
}
}
impl Deref for Child {
type Target = _Child;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Child {
fn deref_mut(&mut self) -> &mut _Child {
&mut self.0
}
}
#[cfg(test)]
mod tests;