Unify server pinging implementations between app and backend (#5510)
* Improve ping impl to bring parity to app lib impl * Fix issue with new impl * fix labrinth compile * wip: why do servers not provide server info.. * Fix ping impl overriding port * fix theseus_gui * remove unneeded recursion lmit
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -10189,6 +10189,7 @@ dependencies = [
|
|||||||
"either",
|
"either",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"enumset",
|
"enumset",
|
||||||
|
"eyre",
|
||||||
"flate2",
|
"flate2",
|
||||||
"fs4",
|
"fs4",
|
||||||
"futures",
|
"futures",
|
||||||
|
|||||||
@@ -423,11 +423,11 @@ pub struct JavaServerPingData {
|
|||||||
/// Reported version protocol number of the server.
|
/// Reported version protocol number of the server.
|
||||||
pub version_protocol: i32,
|
pub version_protocol: i32,
|
||||||
/// Description/MOTD of the server as shown in the server list.
|
/// Description/MOTD of the server as shown in the server list.
|
||||||
pub description: String,
|
pub description: Option<serde_json::Value>,
|
||||||
/// Number of players online at the time.
|
/// Number of players online at the time.
|
||||||
pub players_online: i32,
|
pub players_online: Option<i32>,
|
||||||
/// Maximum number of players allowed on the server.
|
/// Maximum number of players allowed on the server.
|
||||||
pub players_max: i32,
|
pub players_max: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
component::relations! {
|
component::relations! {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use crate::models::exp;
|
|||||||
use crate::models::ids::ProjectId;
|
use crate::models::ids::ProjectId;
|
||||||
use crate::models::projects::ProjectStatus;
|
use crate::models::projects::ProjectStatus;
|
||||||
use crate::{database::PgPool, util::error::Context};
|
use crate::{database::PgPool, util::error::Context};
|
||||||
use async_minecraft_ping::ServerDescription;
|
|
||||||
use chrono::{TimeDelta, Utc};
|
use chrono::{TimeDelta, Utc};
|
||||||
use clickhouse::{Client, Row};
|
use clickhouse::{Client, Row};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -107,11 +106,16 @@ impl ServerPingQueue {
|
|||||||
project_id: project_id.0,
|
project_id: project_id.0,
|
||||||
address: ping.address.clone(),
|
address: ping.address.clone(),
|
||||||
latency_ms: data.map(|d| d.latency.as_millis() as u32),
|
latency_ms: data.map(|d| d.latency.as_millis() as u32),
|
||||||
description: data.map(|d| d.description.clone()),
|
description: data.and_then(|d| {
|
||||||
|
d.description.as_ref().map(|d| {
|
||||||
|
serde_json::to_string(&d)
|
||||||
|
.expect("serialization should not fail")
|
||||||
|
})
|
||||||
|
}),
|
||||||
version_name: data.map(|d| d.version_name.clone()),
|
version_name: data.map(|d| d.version_name.clone()),
|
||||||
version_protocol: data.map(|d| d.version_protocol),
|
version_protocol: data.map(|d| d.version_protocol),
|
||||||
players_online: data.map(|d| d.players_online),
|
players_online: data.and_then(|d| d.players_online),
|
||||||
players_max: data.map(|d| d.players_max),
|
players_max: data.and_then(|d| d.players_max),
|
||||||
};
|
};
|
||||||
|
|
||||||
ch.write(&row)
|
ch.write(&row)
|
||||||
@@ -256,45 +260,27 @@ pub async fn ping_server(
|
|||||||
.map(|duration| duration.min(default_duration))
|
.map(|duration| duration.min(default_duration))
|
||||||
.unwrap_or(default_duration);
|
.unwrap_or(default_duration);
|
||||||
|
|
||||||
let (address, port) = match address.rsplit_once(':') {
|
let conn = async_minecraft_ping::ConnectionConfig::build(address)
|
||||||
Some((addr, port)) => {
|
.with_srv_lookup()
|
||||||
let port = port.parse::<u16>().wrap_err("invalid port number")?;
|
.with_timeout(timeout)
|
||||||
(addr, port)
|
.connect()
|
||||||
}
|
|
||||||
None => (address, 25565),
|
|
||||||
};
|
|
||||||
|
|
||||||
let task = async move {
|
|
||||||
let conn = async_minecraft_ping::ConnectionConfig::build(address)
|
|
||||||
.with_port(port)
|
|
||||||
.with_srv_lookup()
|
|
||||||
.connect()
|
|
||||||
.await
|
|
||||||
.wrap_err("failed to connect to server")?;
|
|
||||||
|
|
||||||
let status = conn
|
|
||||||
.status()
|
|
||||||
.await
|
|
||||||
.wrap_err("failed to get server status")?
|
|
||||||
.status;
|
|
||||||
|
|
||||||
eyre::Ok(exp::minecraft::JavaServerPingData {
|
|
||||||
latency: start.elapsed(),
|
|
||||||
version_name: status.version.name,
|
|
||||||
version_protocol: status.version.protocol,
|
|
||||||
description: match status.description {
|
|
||||||
ServerDescription::Plain(text)
|
|
||||||
| ServerDescription::Object { text } => text,
|
|
||||||
},
|
|
||||||
players_online: status.players.online,
|
|
||||||
players_max: status.players.max,
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
tokio::time::timeout(timeout, task)
|
|
||||||
.await
|
.await
|
||||||
.map_err(eyre::Error::new)
|
.wrap_err("failed to connect to server")?;
|
||||||
.flatten()
|
|
||||||
|
let status = conn
|
||||||
|
.status()
|
||||||
|
.await
|
||||||
|
.wrap_err("failed to get server status")?
|
||||||
|
.status;
|
||||||
|
|
||||||
|
eyre::Ok(exp::minecraft::JavaServerPingData {
|
||||||
|
latency: start.elapsed(),
|
||||||
|
version_name: status.version.name,
|
||||||
|
version_protocol: status.version.protocol,
|
||||||
|
description: status.description,
|
||||||
|
players_online: status.players.online,
|
||||||
|
players_max: status.players.max,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Row, Serialize, Clone)]
|
#[derive(Debug, Row, Serialize, Clone)]
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ dunce = { workspace = true }
|
|||||||
either = { workspace = true }
|
either = { workspace = true }
|
||||||
encoding_rs = { workspace = true }
|
encoding_rs = { workspace = true }
|
||||||
enumset = { workspace = true }
|
enumset = { workspace = true }
|
||||||
|
eyre = { workspace = true }
|
||||||
flate2 = { workspace = true }
|
flate2 = { workspace = true }
|
||||||
fs4 = { workspace = true, features = ["tokio"] }
|
fs4 = { workspace = true, features = ["tokio"] }
|
||||||
futures = { workspace = true, features = ["alloc", "async-await"] }
|
futures = { workspace = true, features = ["alloc", "async-await"] }
|
||||||
|
|||||||
@@ -6,14 +6,13 @@ use crate::state::attached_world_data::AttachedWorldData;
|
|||||||
use crate::state::{
|
use crate::state::{
|
||||||
Profile, ProfileInstallStage, attached_world_data, server_join_log,
|
Profile, ProfileInstallStage, attached_world_data, server_join_log,
|
||||||
};
|
};
|
||||||
|
use crate::util::io;
|
||||||
use crate::util::protocol_version::OLD_PROTOCOL_VERSIONS;
|
use crate::util::protocol_version::OLD_PROTOCOL_VERSIONS;
|
||||||
pub use crate::util::protocol_version::ProtocolVersion;
|
pub use crate::util::protocol_version::ProtocolVersion;
|
||||||
pub use crate::util::server_ping::{
|
pub use crate::util::server_ping::{
|
||||||
ServerGameProfile, ServerPlayers, ServerStatus, ServerVersion,
|
ServerGameProfile, ServerPlayers, ServerStatus, ServerVersion,
|
||||||
};
|
};
|
||||||
use crate::util::{io, server_ping};
|
use crate::{Context, ErrorKind, Result, State, launcher};
|
||||||
use crate::{Error, ErrorKind, Result, State, launcher};
|
|
||||||
use async_minecraft_ping::ServerDescription;
|
|
||||||
use async_walkdir::WalkDir;
|
use async_walkdir::WalkDir;
|
||||||
use async_zip::{Compression, ZipEntryBuilder};
|
use async_zip::{Compression, ZipEntryBuilder};
|
||||||
use chrono::{DateTime, Local, TimeZone, Utc};
|
use chrono::{DateTime, Local, TimeZone, Utc};
|
||||||
@@ -910,67 +909,54 @@ pub async fn get_server_status(
|
|||||||
"Pinging {address} with protocol version {protocol_version:?}"
|
"Pinging {address} with protocol version {protocol_version:?}"
|
||||||
);
|
);
|
||||||
|
|
||||||
get_server_status_old(address, protocol_version).await
|
// get_server_status_old(address, protocol_version).await
|
||||||
// get_server_status_new(address, protocol_version).await
|
get_server_status_new(address, protocol_version).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_server_status_old(
|
// async fn _get_server_status_old(
|
||||||
|
// address: &str,
|
||||||
|
// protocol_version: Option<ProtocolVersion>,
|
||||||
|
// ) -> Result<ServerStatus> {
|
||||||
|
// let (original_host, original_port) = parse_server_address(address)?;
|
||||||
|
// let (host, port) =
|
||||||
|
// resolve_server_address(original_host, original_port).await?;
|
||||||
|
// tracing::debug!(
|
||||||
|
// "Pinging {address} with protocol version {protocol_version:?}"
|
||||||
|
// );
|
||||||
|
// server_ping::get_server_status(
|
||||||
|
// &(&host as &str, port),
|
||||||
|
// (original_host, original_port),
|
||||||
|
// protocol_version,
|
||||||
|
// )
|
||||||
|
// .await
|
||||||
|
|
||||||
|
async fn get_server_status_new(
|
||||||
address: &str,
|
address: &str,
|
||||||
protocol_version: Option<ProtocolVersion>,
|
protocol_version: Option<ProtocolVersion>,
|
||||||
) -> Result<ServerStatus> {
|
) -> Result<ServerStatus> {
|
||||||
let (original_host, original_port) = parse_server_address(address)?;
|
|
||||||
let (host, port) =
|
|
||||||
resolve_server_address(original_host, original_port).await?;
|
|
||||||
tracing::debug!(
|
|
||||||
"Pinging {address} with protocol version {protocol_version:?}"
|
|
||||||
);
|
|
||||||
server_ping::get_server_status(
|
|
||||||
&(&host as &str, port),
|
|
||||||
(original_host, original_port),
|
|
||||||
protocol_version,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn _get_server_status_new(
|
|
||||||
address: &str,
|
|
||||||
protocol_version: Option<ProtocolVersion>,
|
|
||||||
) -> Result<ServerStatus> {
|
|
||||||
let (address, port) = match address.rsplit_once(':') {
|
|
||||||
Some((addr, port)) => {
|
|
||||||
let port = port.parse::<u16>().map_err(|_err| {
|
|
||||||
Error::from(ErrorKind::InputError("invalid port number".into()))
|
|
||||||
})?;
|
|
||||||
(addr, port)
|
|
||||||
}
|
|
||||||
None => (address, 25565),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut builder = async_minecraft_ping::ConnectionConfig::build(address)
|
let mut builder = async_minecraft_ping::ConnectionConfig::build(address)
|
||||||
.with_port(port)
|
|
||||||
.with_srv_lookup();
|
.with_srv_lookup();
|
||||||
|
|
||||||
if let Some(version) = protocol_version {
|
if let Some(version) = protocol_version {
|
||||||
builder = builder.with_protocol_version(version.version as usize)
|
builder = builder.with_protocol_version(version.version as usize)
|
||||||
}
|
}
|
||||||
|
|
||||||
let conn = builder.connect().await.map_err(|_err| {
|
let conn = builder
|
||||||
Error::from(ErrorKind::InputError("failed to connect to server".into()))
|
.connect()
|
||||||
})?;
|
.await
|
||||||
|
.wrap_err("failed to connect to server")?;
|
||||||
|
|
||||||
let ping_conn = conn.status().await.map_err(|_err| {
|
let ping_conn = conn
|
||||||
Error::from(ErrorKind::InputError("failed to get server status".into()))
|
.status()
|
||||||
})?;
|
.await
|
||||||
|
.wrap_err("failed to get server status")?;
|
||||||
let status = &ping_conn.status;
|
let status = &ping_conn.status;
|
||||||
let description = match &status.description {
|
let description = status.description.as_ref().map(|d| {
|
||||||
ServerDescription::Plain(text) => {
|
let json =
|
||||||
serde_json::value::to_raw_value(&text).ok()
|
serde_json::to_string(d).expect("serializing should not fail");
|
||||||
}
|
RawValue::from_string(json)
|
||||||
ServerDescription::Object { text } => {
|
.expect("converting to `RawValue` should not fail")
|
||||||
// TODO: `text` always seems to be empty?
|
});
|
||||||
RawValue::from_string(text.clone()).ok()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let players = ServerPlayers {
|
let players = ServerPlayers {
|
||||||
max: status.players.max,
|
max: status.players.max,
|
||||||
@@ -1000,9 +986,10 @@ async fn _get_server_status_new(
|
|||||||
let latency = {
|
let latency = {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let ping_magic = Utc::now().timestamp_millis().cast_unsigned();
|
let ping_magic = Utc::now().timestamp_millis().cast_unsigned();
|
||||||
ping_conn.ping(ping_magic).await.map_err(|_err| {
|
ping_conn
|
||||||
Error::from(ErrorKind::InputError("failed to do ping".into()))
|
.ping(ping_magic)
|
||||||
})?;
|
.await
|
||||||
|
.wrap_err("failed to do ping")?;
|
||||||
start.elapsed().as_millis() as i64
|
start.elapsed().as_millis() as i64
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
//! Theseus error type
|
//! Theseus error type
|
||||||
use std::sync::Arc;
|
use std::{
|
||||||
|
convert::Infallible,
|
||||||
|
fmt::{Debug, Display},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{profile, util};
|
use crate::{profile, util};
|
||||||
use data_url::DataUrlError;
|
use data_url::DataUrlError;
|
||||||
@@ -220,4 +224,41 @@ impl ErrorKind {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = core::result::Result<T, Error>;
|
pub type Result<T, E = Error> = core::result::Result<T, E>;
|
||||||
|
|
||||||
|
pub trait Context<T, E>: Sized {
|
||||||
|
fn wrap_err_with<D>(self, f: impl FnOnce() -> D) -> Result<T, Error>
|
||||||
|
where
|
||||||
|
D: Send + Sync + Debug + Display + 'static;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn wrap_err<D>(self, msg: D) -> Result<T, Error>
|
||||||
|
where
|
||||||
|
D: Send + Sync + Debug + Display + 'static,
|
||||||
|
{
|
||||||
|
self.wrap_err_with(|| msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, E> Context<T, E> for Result<T, E>
|
||||||
|
where
|
||||||
|
Self: eyre::WrapErr<T, E>,
|
||||||
|
{
|
||||||
|
fn wrap_err_with<D>(self, f: impl FnOnce() -> D) -> Result<T, Error>
|
||||||
|
where
|
||||||
|
D: Send + Sync + Debug + Display + 'static,
|
||||||
|
{
|
||||||
|
eyre::WrapErr::wrap_err_with(self, f).map_err(|err| {
|
||||||
|
Error::from(ErrorKind::OtherError(format!("{err:#}")))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Context<T, Infallible> for Option<T> {
|
||||||
|
fn wrap_err_with<D>(self, f: impl FnOnce() -> D) -> Result<T, Error>
|
||||||
|
where
|
||||||
|
D: Send + Sync + Debug + Display + 'static,
|
||||||
|
{
|
||||||
|
self.ok_or_else(|| Error::from(ErrorKind::OtherError(f().to_string())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ macro_rules! get_resource_file {
|
|||||||
Ok(dir) => dir,
|
Ok(dir) => dir,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
break 'get_resource_file $crate::Result::Err(
|
break 'get_resource_file $crate::Result::Err(
|
||||||
$crate::util::io::IOError::from(e).into(),
|
$crate::Error::from($crate::util::io::IOError::from(e)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,53 +1,3 @@
|
|||||||
use crate::ErrorKind;
|
|
||||||
use crate::error::Result;
|
|
||||||
use crate::util::protocol_version::ProtocolVersion;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_json::value::RawValue;
|
|
||||||
use std::time::Duration;
|
|
||||||
use tokio::net::ToSocketAddrs;
|
|
||||||
use tokio::select;
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct ServerStatus {
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub description: Option<Box<RawValue>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub players: Option<ServerPlayers>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub version: Option<ServerVersion>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub favicon: Option<Url>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub enforces_secure_chat: bool,
|
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub ping: Option<i64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
||||||
pub struct ServerPlayers {
|
|
||||||
pub max: i32,
|
|
||||||
pub online: i32,
|
|
||||||
#[serde(default)]
|
|
||||||
pub sample: Vec<ServerGameProfile>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
||||||
pub struct ServerGameProfile {
|
|
||||||
pub id: String,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
||||||
pub struct ServerVersion {
|
|
||||||
pub name: String,
|
|
||||||
pub protocol: i32,
|
|
||||||
#[serde(skip_deserializing)]
|
|
||||||
pub legacy: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_server_status(
|
pub async fn get_server_status(
|
||||||
address: &impl ToSocketAddrs,
|
address: &impl ToSocketAddrs,
|
||||||
original_address: (&str, u16),
|
original_address: (&str, u16),
|
||||||
@@ -305,8 +255,8 @@ mod legacy {
|
|||||||
}),
|
}),
|
||||||
description: parts.next().and_then(|x| to_raw_value(x).ok()),
|
description: parts.next().and_then(|x| to_raw_value(x).ok()),
|
||||||
players: Some(ServerPlayers {
|
players: Some(ServerPlayers {
|
||||||
online: parts.next().and_then(|x| x.parse().ok()).unwrap_or(-1),
|
online: parts.next().and_then(|x| x.parse().ok()),
|
||||||
max: parts.next().and_then(|x| x.parse().ok()).unwrap_or(-1),
|
max: parts.next().and_then(|x| x.parse().ok()),
|
||||||
sample: vec![],
|
sample: vec![],
|
||||||
}),
|
}),
|
||||||
favicon: None,
|
favicon: None,
|
||||||
43
packages/app-lib/src/util/server_ping/mod.rs
Normal file
43
packages/app-lib/src/util/server_ping/mod.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::value::RawValue;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ServerStatus {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub description: Option<Box<RawValue>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub players: Option<ServerPlayers>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub version: Option<ServerVersion>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub favicon: Option<Url>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub enforces_secure_chat: bool,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub ping: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
|
pub struct ServerPlayers {
|
||||||
|
pub max: Option<i32>,
|
||||||
|
pub online: Option<i32>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub sample: Vec<ServerGameProfile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
|
pub struct ServerGameProfile {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
|
pub struct ServerVersion {
|
||||||
|
pub name: String,
|
||||||
|
pub protocol: i32,
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
pub legacy: bool,
|
||||||
|
}
|
||||||
@@ -37,10 +37,12 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
let connection = connection.status().await?;
|
let connection = connection.status().await?;
|
||||||
|
|
||||||
println!(
|
if let (Some(online), Some(max)) = (
|
||||||
"{} of {} player(s) online",
|
connection.status.players.online,
|
||||||
connection.status.players.online, connection.status.players.max
|
connection.status.players.max,
|
||||||
);
|
) {
|
||||||
|
println!("{online} of {max} player(s) online");
|
||||||
|
}
|
||||||
|
|
||||||
connection.ping(42).await?;
|
connection.ping(42).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
mod protocol;
|
mod protocol;
|
||||||
mod server;
|
mod server;
|
||||||
pub use server::{
|
pub use server::{
|
||||||
connect, ConnectionConfig, ServerDescription, ServerError, ServerPlayer, ServerPlayers,
|
connect, ConnectionConfig, ServerError, ServerPlayer, ServerPlayers, ServerVersion,
|
||||||
ServerVersion, StatusConnection, StatusResponse,
|
StatusConnection, StatusResponse,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ impl From<protocol::ProtocolError> for ServerError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Contains information about the server version.
|
/// Contains information about the server version.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct ServerVersion {
|
pub struct ServerVersion {
|
||||||
/// The server's Minecraft version, i.e. "1.15.2".
|
/// The server's Minecraft version, i.e. "1.15.2".
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -44,7 +44,7 @@ pub struct ServerVersion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Contains information about a player.
|
/// Contains information about a player.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct ServerPlayer {
|
pub struct ServerPlayer {
|
||||||
/// The player's in-game name.
|
/// The player's in-game name.
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -55,40 +55,33 @@ pub struct ServerPlayer {
|
|||||||
|
|
||||||
/// Contains information about the currently online
|
/// Contains information about the currently online
|
||||||
/// players.
|
/// players.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||||
pub struct ServerPlayers {
|
pub struct ServerPlayers {
|
||||||
/// The configured maximum number of players for the
|
/// The configured maximum number of players for the
|
||||||
/// server.
|
/// server.
|
||||||
pub max: i32,
|
pub max: Option<i32>,
|
||||||
|
|
||||||
/// The number of players currently online.
|
/// The number of players currently online.
|
||||||
pub online: i32,
|
pub online: Option<i32>,
|
||||||
|
|
||||||
/// An optional list of player information for
|
/// An optional list of player information for
|
||||||
/// currently online players.
|
/// currently online players.
|
||||||
pub sample: Option<Vec<ServerPlayer>>,
|
pub sample: Option<Vec<ServerPlayer>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Contains the server's MOTD.
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum ServerDescription {
|
|
||||||
Plain(String),
|
|
||||||
Object { text: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The decoded JSON response from a status query over
|
/// The decoded JSON response from a status query over
|
||||||
/// ServerListPing.
|
/// ServerListPing.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct StatusResponse {
|
pub struct StatusResponse {
|
||||||
/// Information about the server's version.
|
/// Information about the server's version.
|
||||||
pub version: ServerVersion,
|
pub version: ServerVersion,
|
||||||
|
|
||||||
/// Information about currently online players.
|
/// Information about currently online players.
|
||||||
|
#[serde(default)]
|
||||||
pub players: ServerPlayers,
|
pub players: ServerPlayers,
|
||||||
|
|
||||||
/// Single-field struct containing the server's MOTD.
|
/// Single-field struct containing the server's MOTD.
|
||||||
pub description: ServerDescription,
|
pub description: Option<serde_json::Value>,
|
||||||
|
|
||||||
/// Optional field containing a path to the server's
|
/// Optional field containing a path to the server's
|
||||||
/// favicon.
|
/// favicon.
|
||||||
@@ -104,7 +97,7 @@ const DEFAULT_TIMEOUT: Duration = Duration::from_secs(2);
|
|||||||
pub struct ConnectionConfig {
|
pub struct ConnectionConfig {
|
||||||
protocol_version: usize,
|
protocol_version: usize,
|
||||||
address: String,
|
address: String,
|
||||||
port: u16,
|
port_override: Option<u16>,
|
||||||
timeout: Duration,
|
timeout: Duration,
|
||||||
#[cfg(feature = "srv")]
|
#[cfg(feature = "srv")]
|
||||||
srv_lookup: bool,
|
srv_lookup: bool,
|
||||||
@@ -113,11 +106,19 @@ pub struct ConnectionConfig {
|
|||||||
impl ConnectionConfig {
|
impl ConnectionConfig {
|
||||||
/// Initiates the Minecraft server
|
/// Initiates the Minecraft server
|
||||||
/// connection build process.
|
/// connection build process.
|
||||||
pub fn build<T: Into<String>>(address: T) -> Self {
|
pub fn build(address: impl AsRef<str>) -> Self {
|
||||||
|
let (address, port_override) = match address.as_ref().rsplit_once(':') {
|
||||||
|
Some((addr, port)) => match port.parse::<u16>() {
|
||||||
|
Ok(port) => (addr, Some(port)),
|
||||||
|
Err(_) => (addr, None),
|
||||||
|
},
|
||||||
|
None => (address.as_ref(), None),
|
||||||
|
};
|
||||||
|
|
||||||
ConnectionConfig {
|
ConnectionConfig {
|
||||||
protocol_version: LATEST_PROTOCOL_VERSION,
|
protocol_version: LATEST_PROTOCOL_VERSION,
|
||||||
address: address.into(),
|
address: address.to_string(),
|
||||||
port: DEFAULT_PORT,
|
port_override,
|
||||||
timeout: DEFAULT_TIMEOUT,
|
timeout: DEFAULT_TIMEOUT,
|
||||||
#[cfg(feature = "srv")]
|
#[cfg(feature = "srv")]
|
||||||
srv_lookup: false,
|
srv_lookup: false,
|
||||||
@@ -137,7 +138,7 @@ impl ConnectionConfig {
|
|||||||
/// connection to use. If not specified, the
|
/// connection to use. If not specified, the
|
||||||
/// default port of 25565 will be used.
|
/// default port of 25565 will be used.
|
||||||
pub fn with_port(mut self, port: u16) -> Self {
|
pub fn with_port(mut self, port: u16) -> Self {
|
||||||
self.port = port;
|
self.port_override = Some(port);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +166,8 @@ impl ConnectionConfig {
|
|||||||
|
|
||||||
/// Connects to the server and consumes the builder.
|
/// Connects to the server and consumes the builder.
|
||||||
pub async fn connect(self) -> Result<StatusConnection, ServerError> {
|
pub async fn connect(self) -> Result<StatusConnection, ServerError> {
|
||||||
let (address, port) = self.resolve_address().await;
|
let (address, resolved_port) = self.resolve_address().await;
|
||||||
|
let port = self.port_override.or(resolved_port).unwrap_or(DEFAULT_PORT);
|
||||||
|
|
||||||
let stream = tokio::time::timeout(
|
let stream = tokio::time::timeout(
|
||||||
self.timeout,
|
self.timeout,
|
||||||
@@ -185,43 +187,43 @@ impl ConnectionConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "srv")]
|
#[cfg(feature = "srv")]
|
||||||
async fn resolve_address(&self) -> (String, u16) {
|
async fn resolve_address(&self) -> (String, Option<u16>) {
|
||||||
if !self.srv_lookup {
|
if !self.srv_lookup {
|
||||||
return (self.address.clone(), self.port);
|
return (self.address.clone(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to resolve SRV record, fall back to original address on any failure
|
// Try to resolve SRV record, fall back to original address on any failure
|
||||||
match self.lookup_srv().await {
|
match lookup_srv(&self.address, self.timeout).await {
|
||||||
Some((host, port)) => (host, port),
|
Some((host, port)) => (host, Some(port)),
|
||||||
None => (self.address.clone(), self.port),
|
None => (self.address.clone(), None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "srv"))]
|
#[cfg(not(feature = "srv"))]
|
||||||
async fn resolve_address(&self) -> (String, u16) {
|
async fn resolve_address(&self) -> (String, Option<u16>) {
|
||||||
(self.address.clone(), self.port)
|
(self.address.clone(), None)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "srv")]
|
#[cfg(feature = "srv")]
|
||||||
async fn lookup_srv(&self) -> Option<(String, u16)> {
|
async fn lookup_srv(address: &str, timeout: Duration) -> Option<(String, u16)> {
|
||||||
use hickory_resolver::TokioAsyncResolver;
|
use hickory_resolver::TokioAsyncResolver;
|
||||||
|
|
||||||
let resolver = TokioAsyncResolver::tokio_from_system_conf().ok()?;
|
let resolver = TokioAsyncResolver::tokio_from_system_conf().ok()?;
|
||||||
let srv_name = format!("_minecraft._tcp.{}", self.address);
|
let srv_name = format!("_minecraft._tcp.{address}");
|
||||||
|
|
||||||
let lookup = tokio::time::timeout(self.timeout, resolver.srv_lookup(&srv_name))
|
let lookup = tokio::time::timeout(timeout, resolver.srv_lookup(&srv_name))
|
||||||
.await
|
.await
|
||||||
.ok()?
|
.ok()?
|
||||||
.ok()?;
|
.ok()?;
|
||||||
|
|
||||||
let record = lookup.iter().next()?;
|
let record = lookup.iter().next()?;
|
||||||
let target = record.target().to_string();
|
let target = record.target().to_string();
|
||||||
// Remove trailing dot from DNS name
|
// Remove trailing dot from DNS name
|
||||||
let host = target.trim_end_matches('.').to_string();
|
let host = target.trim_end_matches('.').to_string();
|
||||||
let port = record.port();
|
let port = record.port();
|
||||||
|
|
||||||
Some((host, port))
|
Some((host, port))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience wrapper for easily connecting
|
/// Convenience wrapper for easily connecting
|
||||||
@@ -325,22 +327,24 @@ impl PingConnection {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_server_description_plain() {
|
|
||||||
let json = r#""A Minecraft Server""#;
|
|
||||||
let desc: ServerDescription = serde_json::from_str(json).unwrap();
|
|
||||||
assert!(matches!(desc, ServerDescription::Plain(s) if s == "A Minecraft Server"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_server_description_object() {
|
|
||||||
let json = r#"{"text":"A Minecraft Server"}"#;
|
|
||||||
let desc: ServerDescription = serde_json::from_str(json).unwrap();
|
|
||||||
assert!(matches!(desc, ServerDescription::Object { text } if text == "A Minecraft Server"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_status_response_minimal() {
|
fn test_status_response_minimal() {
|
||||||
|
let json = r#"{
|
||||||
|
"version": {"name": "1.20.4", "protocol": 765}
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let response: StatusResponse = serde_json::from_str(json).unwrap();
|
||||||
|
assert_eq!(response.version.name, "1.20.4");
|
||||||
|
assert_eq!(response.version.protocol, 765);
|
||||||
|
assert_eq!(response.players.max, None);
|
||||||
|
assert_eq!(response.players.online, None);
|
||||||
|
assert!(response.description.is_none());
|
||||||
|
assert!(response.players.sample.is_none());
|
||||||
|
assert!(response.favicon.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_status_response_small() {
|
||||||
let json = r#"{
|
let json = r#"{
|
||||||
"version": {"name": "1.20.4", "protocol": 765},
|
"version": {"name": "1.20.4", "protocol": 765},
|
||||||
"players": {"max": 20, "online": 5},
|
"players": {"max": 20, "online": 5},
|
||||||
@@ -350,8 +354,8 @@ mod tests {
|
|||||||
let response: StatusResponse = serde_json::from_str(json).unwrap();
|
let response: StatusResponse = serde_json::from_str(json).unwrap();
|
||||||
assert_eq!(response.version.name, "1.20.4");
|
assert_eq!(response.version.name, "1.20.4");
|
||||||
assert_eq!(response.version.protocol, 765);
|
assert_eq!(response.version.protocol, 765);
|
||||||
assert_eq!(response.players.max, 20);
|
assert_eq!(response.players.max, Some(20));
|
||||||
assert_eq!(response.players.online, 5);
|
assert_eq!(response.players.online, Some(5));
|
||||||
assert!(response.players.sample.is_none());
|
assert!(response.players.sample.is_none());
|
||||||
assert!(response.favicon.is_none());
|
assert!(response.favicon.is_none());
|
||||||
}
|
}
|
||||||
@@ -396,15 +400,21 @@ mod tests {
|
|||||||
fn test_connection_config_defaults() {
|
fn test_connection_config_defaults() {
|
||||||
let config = ConnectionConfig::build("localhost");
|
let config = ConnectionConfig::build("localhost");
|
||||||
assert_eq!(config.address, "localhost");
|
assert_eq!(config.address, "localhost");
|
||||||
assert_eq!(config.port, DEFAULT_PORT);
|
assert_eq!(config.port_override, None);
|
||||||
assert_eq!(config.timeout, DEFAULT_TIMEOUT);
|
assert_eq!(config.timeout, DEFAULT_TIMEOUT);
|
||||||
assert_eq!(config.protocol_version, LATEST_PROTOCOL_VERSION);
|
assert_eq!(config.protocol_version, LATEST_PROTOCOL_VERSION);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connection_config_with_port_in_address() {
|
||||||
|
let config = ConnectionConfig::build("localhost:12345");
|
||||||
|
assert_eq!(config.port_override, Some(12345));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_connection_config_with_port() {
|
fn test_connection_config_with_port() {
|
||||||
let config = ConnectionConfig::build("localhost").with_port(12345);
|
let config = ConnectionConfig::build("localhost").with_port(12345);
|
||||||
assert_eq!(config.port, 12345);
|
assert_eq!(config.port_override, Some(12345));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -250,8 +250,8 @@ async fn test_status_json_parsing_plain_description() {
|
|||||||
let response: StatusResponse = serde_json::from_str(json).unwrap();
|
let response: StatusResponse = serde_json::from_str(json).unwrap();
|
||||||
assert_eq!(response.version.name, "1.20.4");
|
assert_eq!(response.version.name, "1.20.4");
|
||||||
assert_eq!(response.version.protocol, 765);
|
assert_eq!(response.version.protocol, 765);
|
||||||
assert_eq!(response.players.max, 100);
|
assert_eq!(response.players.max, Some(100));
|
||||||
assert_eq!(response.players.online, 42);
|
assert_eq!(response.players.online, Some(42));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -267,7 +267,7 @@ async fn test_status_json_parsing_object_description() {
|
|||||||
|
|
||||||
let response: StatusResponse = serde_json::from_str(json).unwrap();
|
let response: StatusResponse = serde_json::from_str(json).unwrap();
|
||||||
assert_eq!(response.version.name, "1.19.4");
|
assert_eq!(response.version.name, "1.19.4");
|
||||||
assert_eq!(response.players.online, 10);
|
assert_eq!(response.players.online, Some(10));
|
||||||
assert!(response.players.sample.is_some());
|
assert!(response.players.sample.is_some());
|
||||||
assert_eq!(response.players.sample.as_ref().unwrap().len(), 1);
|
assert_eq!(response.players.sample.as_ref().unwrap().len(), 1);
|
||||||
assert_eq!(response.players.sample.as_ref().unwrap()[0].name, "Notch");
|
assert_eq!(response.players.sample.as_ref().unwrap()[0].name, "Notch");
|
||||||
|
|||||||
Reference in New Issue
Block a user