mirror of
https://github.com/italicsjenga/valence.git
synced 2024-12-23 06:21:31 +11:00
new packet inspector (#369)
## Description Describe the changes you've made. Link to any issues this PR fixes or addresses. ... changes... wel, lets see... uuhhmmm, i rewrote the entire packet inspector :) (also dont mind the snarkyness of that sentence, im tired) closes #346
This commit is contained in:
parent
2ed5a8840d
commit
8712fea19e
15
Cargo.toml
15
Cargo.toml
|
@ -1,6 +1,6 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["crates/*", "tools/*"]
|
members = ["crates/*", "tools/*"]
|
||||||
exclude = ["rust-mc-bot", "tools/stresser", "tools/packet_inspector"]
|
exclude = ["rust-mc-bot", "tools/stresser"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
@ -19,7 +19,9 @@ async-trait = "0.1.60"
|
||||||
atty = "0.2.14"
|
atty = "0.2.14"
|
||||||
base64 = "0.21.0"
|
base64 = "0.21.0"
|
||||||
bevy_app = { version = "0.10.1", default-features = false }
|
bevy_app = { version = "0.10.1", default-features = false }
|
||||||
bevy_ecs = { version = "0.10.1", default-features = false, features = ["trace"] }
|
bevy_ecs = { version = "0.10.1", default-features = false, features = [
|
||||||
|
"trace",
|
||||||
|
] }
|
||||||
bevy_hierarchy = { version = "0.10.1", default-features = false }
|
bevy_hierarchy = { version = "0.10.1", default-features = false }
|
||||||
bevy_mod_debugdump = "0.7.0"
|
bevy_mod_debugdump = "0.7.0"
|
||||||
bitfield-struct = "0.3.1"
|
bitfield-struct = "0.3.1"
|
||||||
|
@ -30,9 +32,10 @@ cfb8 = "0.8.1"
|
||||||
clap = { version = "4.0.30", features = ["derive"] }
|
clap = { version = "4.0.30", features = ["derive"] }
|
||||||
criterion = "0.4.0"
|
criterion = "0.4.0"
|
||||||
directories = "5.0.0"
|
directories = "5.0.0"
|
||||||
eframe = { version = "0.21.0", default-features = false }
|
eframe = { version = "0.22.0", default-features = false }
|
||||||
egui = "0.21.0"
|
egui = "0.22.0"
|
||||||
enum-map = "2.4.2"
|
egui_dock = "0.6"
|
||||||
|
enum-map = "2.5.0"
|
||||||
flate2 = "1.0.24"
|
flate2 = "1.0.24"
|
||||||
flume = "0.10.14"
|
flume = "0.10.14"
|
||||||
fs_extra = "1.2.0"
|
fs_extra = "1.2.0"
|
||||||
|
@ -40,6 +43,8 @@ glam = "0.23.0"
|
||||||
heck = "0.4.0"
|
heck = "0.4.0"
|
||||||
hmac = "0.12.1"
|
hmac = "0.12.1"
|
||||||
indexmap = "1.9.3"
|
indexmap = "1.9.3"
|
||||||
|
image = "0.24.6"
|
||||||
|
itertools = "0.10.5"
|
||||||
lru = "0.10.0"
|
lru = "0.10.0"
|
||||||
noise = "0.8.2"
|
noise = "0.8.2"
|
||||||
num = "0.4.0"
|
num = "0.4.0"
|
||||||
|
|
|
@ -5,32 +5,50 @@ edition.workspace = true
|
||||||
description = "A simple Minecraft proxy for inspecting packets."
|
description = "A simple Minecraft proxy for inspecting packets."
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["syntax_highlighting"]
|
default = ["gui"]
|
||||||
syntax_highlighting = ["syntect"]
|
gui = [
|
||||||
|
"image",
|
||||||
|
"syntect",
|
||||||
|
"serde",
|
||||||
|
"egui",
|
||||||
|
"eframe",
|
||||||
|
"egui_dock",
|
||||||
|
"itertools",
|
||||||
|
"enum-map",
|
||||||
|
]
|
||||||
|
cli = ["clap"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow = { workspace = true }
|
||||||
atty.workspace = true
|
bytes = { workspace = true }
|
||||||
bytes.workspace = true
|
flate2 = { workspace = true }
|
||||||
clap.workspace = true
|
flume = { workspace = true }
|
||||||
directories.workspace = true
|
tokio = { workspace = true, features = ["full"] }
|
||||||
eframe = { workspace = true, default-features = false, features = [
|
tracing = { workspace = true }
|
||||||
"default_fonts", # Embed the default egui fonts.
|
valence = { workspace = true }
|
||||||
"glow", # Use the glow rendering backend. Alternative: "wgpu".
|
time = { workspace = true, features = ["local-offset"] }
|
||||||
|
image = { workspace = true, optional = true }
|
||||||
|
tracing-subscriber = { workspace = true }
|
||||||
|
egui = { workspace = true, optional = true }
|
||||||
|
eframe = { workspace = true, optional = true, features = [
|
||||||
|
"persistence",
|
||||||
|
"wgpu",
|
||||||
] }
|
] }
|
||||||
egui.workspace = true
|
egui_dock = { workspace = true, optional = true, features = ["serde"] }
|
||||||
enum-map.workspace = true
|
itertools = { workspace = true, optional = true }
|
||||||
owo-colors.workspace = true
|
enum-map = { workspace = true, optional = true, features = ["serde"] }
|
||||||
regex.workspace = true
|
syntect = { workspace = true, default-features = false, optional = true, features = [
|
||||||
rfd.workspace = true
|
|
||||||
serde = { workspace = true, features = ["derive"] }
|
|
||||||
syntect = { workspace = true, optional = true, default-features = false, features = [
|
|
||||||
"default-fancy",
|
"default-fancy",
|
||||||
] }
|
] }
|
||||||
time = { workspace = true, features = ["local-offset"] }
|
serde = { workspace = true, optional = true, features = ["derive"] }
|
||||||
tokio.workspace = true
|
clap = { workspace = true, optional = true, features = ["derive"] }
|
||||||
toml.workspace = true
|
|
||||||
tracing-subscriber.workspace = true
|
|
||||||
valence_nbt = { workspace = true, features = ["preserve_order"] }
|
|
||||||
valence.workspace = true
|
|
||||||
|
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
syn = "2.0.18"
|
||||||
|
anyhow = "1.0.71"
|
||||||
|
heck = "0.4.1"
|
||||||
|
proc-macro2 = "1.0.60"
|
||||||
|
quote = "1.0.28"
|
||||||
|
serde = { version = "1.0.164", features = ["derive"] }
|
||||||
|
serde_json = "1.0.96"
|
||||||
|
|
|
@ -19,70 +19,37 @@ cargo r -r --example conway
|
||||||
```
|
```
|
||||||
|
|
||||||
Next up, we need to run the proxy server, this can be done in 2 different ways,
|
Next up, we need to run the proxy server, this can be done in 2 different ways,
|
||||||
either using the GUI application (default) or using the `--nogui` flag to log
|
either using the GUI application (default) or by using the `cli` feature gate.
|
||||||
the packets to a terminal instance.
|
|
||||||
|
To launch in a Gui environment, simply run `packet_inspector`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo r -r -p packet_inspector
|
||||||
|
```
|
||||||
|
|
||||||
|
To Launch in a Cli environment, build `packet_inspector` with the default
|
||||||
|
features disabled, and supplying the `cli` feature. note that you **must**
|
||||||
|
supply the listener and server addresses as arguments.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo r -r -p packet_inspector --no-default-features --features cli -- 127.0.0.1:25566 127.0.0.1:25565
|
||||||
|
```
|
||||||
|
|
||||||
To assist, `--help` will produce the following:
|
To assist, `--help` will produce the following:
|
||||||
|
|
||||||
```
|
```
|
||||||
A simple Minecraft proxy for inspecting packets.
|
A simple Minecraft proxy for inspecting packets.
|
||||||
|
|
||||||
Usage: packet_inspector [OPTIONS] [CLIENT_ADDR] [SERVER_ADDR]
|
Usage: packet_inspector <LISTENER_ADDR> <SERVER_ADDR>
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
[CLIENT_ADDR] The socket address to listen for connections on. This is the address clients should connect to
|
<LISTENER_ADDR> The socket address to listen for connections on. This is the address clients should connect to
|
||||||
[SERVER_ADDR] The socket address the proxy will connect to. This is the address of the server
|
<SERVER_ADDR> The socket address the proxy will connect to. This is the address of the server
|
||||||
|
|
||||||
Options:
|
|
||||||
-m, --max-connections <MAX_CONNECTIONS>
|
|
||||||
The maximum number of connections allowed to the proxy. By default, there is no limit
|
|
||||||
--nogui
|
|
||||||
Disable the GUI. Logging to stdout
|
|
||||||
-i, --include-filter <INCLUDE_FILTER>
|
|
||||||
Only show packets that match the filter
|
|
||||||
-e, --exclude-filter <EXCLUDE_FILTER>
|
|
||||||
Hide packets that match the filter. Note: Only in effect if nogui is set
|
|
||||||
-h, --help
|
|
||||||
Print help
|
|
||||||
-V, --version
|
|
||||||
Print version
|
|
||||||
```
|
```
|
||||||
|
|
||||||
To launch in a Gui environment, simply launch `packet_inspector[.exe]` (or
|
The client can now connect to `localhost:25566`. You should see the packets in
|
||||||
`cargo r -r -p packet_inspector` to run from source). The gui will prompt you
|
`stdout` when running in cli mode, or you should see packets streaming in on the
|
||||||
for the `CLIENT_ADDR` and `SERVER_ADDR` if they have not been supplied via the
|
Gui.
|
||||||
command line arguments.
|
|
||||||
|
|
||||||
In a terminal only environment, use the `--nogui` option and supply
|
|
||||||
`CLIENT_ADDR` and `SERVER_ADDR` as arguments.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo r -r -p packet_inspector -- --nogui 127.0.0.1:25566 127.0.0.1:25565
|
|
||||||
```
|
|
||||||
|
|
||||||
The client must connect to `localhost:25566`. You should see the packets in
|
|
||||||
`stdout` when running in `--nogui`, or you should see packets streaming in on
|
|
||||||
the Gui.
|
|
||||||
|
|
||||||
The `-i` and `-e` flags accept a regex to filter packets according to their
|
|
||||||
name. The `-i` regex includes matching packets while the `-e` regex excludes
|
|
||||||
matching packets. Do note that `-e` only applies in `--nogui` environment, as
|
|
||||||
the Gui has a "packet selector" to enable/disable packets dynamically. The `-i`
|
|
||||||
parameter value will be included in the `Filter` input field on the Gui.
|
|
||||||
|
|
||||||
For instance, if you only want to print the packets `Foo`, `Bar`, and `Baz`, you
|
|
||||||
can use a regex such as `^(Foo|Bar|Baz)$` with the `-i` flag.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo r -r -p packet_inspector -- --nogui 127.0.0.1:25566 127.0.0.1:25565 -i '^(Foo|Bar|Baz)$'
|
|
||||||
```
|
|
||||||
|
|
||||||
Packets are printed to `stdout` while errors are printed to `stderr`. If you
|
|
||||||
only want to see errors in your terminal, direct `stdout` elsewhere.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo r -r -p packet_inspector -- --nogui 127.0.0.1:25566 127.0.0.1:25565 > log.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
## Quick start with Vanilla Server via Docker
|
## Quick start with Vanilla Server via Docker
|
||||||
|
|
||||||
|
@ -107,7 +74,7 @@ docker exec -i mc rcon-cli
|
||||||
In a separate terminal, start the packet inspector.
|
In a separate terminal, start the packet inspector.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo r -r -p packet_inspector -- --nogui 127.0.0.1:25566 127.0.0.1:25565
|
cargo r -r -p packet_inspector --no-default-features --features cli -- 127.0.0.1:25566 127.0.0.1:25565
|
||||||
```
|
```
|
||||||
|
|
||||||
Open Minecraft and connect to `localhost:25566`.
|
Open Minecraft and connect to `localhost:25566`.
|
||||||
|
|
226
tools/packet_inspector/build.rs
Normal file
226
tools/packet_inspector/build.rs
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
use std::{collections::HashMap, env, fs, path::Path, process::Command};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Packet {
|
||||||
|
name: String,
|
||||||
|
side: String,
|
||||||
|
state: String,
|
||||||
|
id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() -> anyhow::Result<()> {
|
||||||
|
let packets: Vec<Packet> = serde_json::from_str(include_str!("../../extracted/packets.json"))?;
|
||||||
|
|
||||||
|
write_packets(&packets)?;
|
||||||
|
write_transformer(&packets)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_packets(packets: &Vec<Packet>) -> anyhow::Result<()> {
|
||||||
|
let mut consts = TokenStream::new();
|
||||||
|
|
||||||
|
let len = packets.len();
|
||||||
|
|
||||||
|
let mut p: Vec<TokenStream> = Vec::new();
|
||||||
|
|
||||||
|
for packet in packets {
|
||||||
|
let name = packet.name.strip_suffix("Packet").unwrap_or(&packet.name);
|
||||||
|
// lowercase the last character of name
|
||||||
|
let name = {
|
||||||
|
let mut chars = name.chars();
|
||||||
|
let last_char = chars.next_back().unwrap();
|
||||||
|
let last_char = last_char.to_lowercase().to_string();
|
||||||
|
let mut name = chars.collect::<String>();
|
||||||
|
name.push_str(&last_char);
|
||||||
|
name
|
||||||
|
};
|
||||||
|
|
||||||
|
// if the packet is clientbound, but the name does not ends with S2c, add it
|
||||||
|
let name = if packet.side == "clientbound" && !name.ends_with("S2c") {
|
||||||
|
format!("{}S2c", name)
|
||||||
|
} else {
|
||||||
|
name
|
||||||
|
};
|
||||||
|
|
||||||
|
// same for serverbound
|
||||||
|
let name = if packet.side == "serverbound" && !name.ends_with("C2s") {
|
||||||
|
format!("{}C2s", name)
|
||||||
|
} else {
|
||||||
|
name
|
||||||
|
};
|
||||||
|
|
||||||
|
let id = packet.id;
|
||||||
|
let side = match &packet.side {
|
||||||
|
s if s == "clientbound" => quote! { crate::packet_registry::PacketSide::Clientbound },
|
||||||
|
s if s == "serverbound" => quote! { crate::packet_registry::PacketSide::Serverbound },
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let state = match &packet.state {
|
||||||
|
s if s == "handshaking" => quote! { crate::packet_registry::PacketState::Handshaking },
|
||||||
|
s if s == "status" => quote! { crate::packet_registry::PacketState::Status },
|
||||||
|
s if s == "login" => quote! { crate::packet_registry::PacketState::Login },
|
||||||
|
s if s == "play" => quote! { crate::packet_registry::PacketState::Play },
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// const STD_PACKETS = [PacketSide::Client(PacketState::Handshaking(Packet{..})), ..];
|
||||||
|
p.push(quote! {
|
||||||
|
crate::packet_registry::Packet {
|
||||||
|
id: #id,
|
||||||
|
side: #side,
|
||||||
|
state: #state,
|
||||||
|
timestamp: None,
|
||||||
|
name: #name,
|
||||||
|
data: None,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
consts.extend([quote! {
|
||||||
|
pub const STD_PACKETS: [crate::packet_registry::Packet; #len] = [
|
||||||
|
#(#p),*
|
||||||
|
];
|
||||||
|
}]);
|
||||||
|
|
||||||
|
write_generated_file(consts, "packets.rs")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_transformer(packets: &[Packet]) -> anyhow::Result<()> {
|
||||||
|
// HashMap<side, HashMap<state, Vec<name>>>
|
||||||
|
let grouped_packets = HashMap::<String, HashMap<String, Vec<String>>>::new();
|
||||||
|
|
||||||
|
let mut grouped_packets = packets.iter().fold(grouped_packets, |mut acc, packet| {
|
||||||
|
let side = match packet.side.as_str() {
|
||||||
|
"serverbound" => "Serverbound".to_string(),
|
||||||
|
"clientbound" => "Clientbound".to_string(),
|
||||||
|
_ => panic!("Invalid side"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let state = match packet.state.as_str() {
|
||||||
|
"handshaking" => "Handshaking".to_string(),
|
||||||
|
"status" => "Status".to_string(),
|
||||||
|
"login" => "Login".to_string(),
|
||||||
|
"play" => "Play".to_string(),
|
||||||
|
_ => panic!("Invalid state"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let name = packet
|
||||||
|
.name
|
||||||
|
.strip_suffix("Packet")
|
||||||
|
.unwrap_or(&packet.name)
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// lowercase the last character of name
|
||||||
|
let name = {
|
||||||
|
let mut chars = name.chars();
|
||||||
|
let last_char = chars.next_back().unwrap();
|
||||||
|
let last_char = last_char.to_lowercase().to_string();
|
||||||
|
let mut name = chars.collect::<String>();
|
||||||
|
name.push_str(&last_char);
|
||||||
|
name
|
||||||
|
};
|
||||||
|
|
||||||
|
// if the packet is clientbound, but the name does not ends with S2c, add it
|
||||||
|
let name = if side == "Clientbound" && !name.ends_with("S2c") {
|
||||||
|
format!("{}S2c", name)
|
||||||
|
} else {
|
||||||
|
name
|
||||||
|
};
|
||||||
|
|
||||||
|
// same for serverbound
|
||||||
|
let name = if side == "Serverbound" && !name.ends_with("C2s") {
|
||||||
|
format!("{}C2s", name)
|
||||||
|
} else {
|
||||||
|
name
|
||||||
|
};
|
||||||
|
|
||||||
|
let state_map = acc.entry(side).or_insert_with(HashMap::new);
|
||||||
|
let id_map = state_map.entry(state).or_insert_with(Vec::new);
|
||||||
|
id_map.push(name);
|
||||||
|
|
||||||
|
acc
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut generated = TokenStream::new();
|
||||||
|
|
||||||
|
for (side, state_map) in grouped_packets.iter_mut() {
|
||||||
|
let mut side_arms = TokenStream::new();
|
||||||
|
for (state, id_map) in state_map.iter_mut() {
|
||||||
|
let mut match_arms = TokenStream::new();
|
||||||
|
|
||||||
|
for name in id_map.iter_mut() {
|
||||||
|
let name = syn::parse_str::<syn::Ident>(name).unwrap();
|
||||||
|
|
||||||
|
match_arms.extend(quote! {
|
||||||
|
#name::ID => {
|
||||||
|
format!("{:#?}", #name::decode(&mut data).unwrap())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = syn::parse_str::<syn::Ident>(state).unwrap();
|
||||||
|
|
||||||
|
side_arms.extend(quote! {
|
||||||
|
PacketState::#state => match packet.id {
|
||||||
|
#match_arms
|
||||||
|
_ => NOT_AVAILABLE.to_string(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if side == "Clientbound" {
|
||||||
|
side_arms.extend(quote! {
|
||||||
|
_ => NOT_AVAILABLE.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let side = syn::parse_str::<syn::Ident>(side).unwrap();
|
||||||
|
|
||||||
|
generated.extend(quote! {
|
||||||
|
PacketSide::#side => match packet.state {
|
||||||
|
#side_arms
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrap generated in a function definition
|
||||||
|
let generated = quote! {
|
||||||
|
const NOT_AVAILABLE: &str = "Not yet implemented";
|
||||||
|
|
||||||
|
pub fn packet_to_string(packet: &ProxyPacket) -> String {
|
||||||
|
let bytes = packet.data.as_ref().unwrap();
|
||||||
|
let mut data = &bytes.clone()[..];
|
||||||
|
|
||||||
|
match packet.side {
|
||||||
|
#generated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
write_generated_file(generated, "packet_to_string.rs")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_generated_file(content: TokenStream, out_file: &str) -> anyhow::Result<()> {
|
||||||
|
let out_dir = env::var_os("OUT_DIR").context("failed to get OUT_DIR env var")?;
|
||||||
|
let path = Path::new(&out_dir).join(out_file);
|
||||||
|
let code = content.to_string();
|
||||||
|
|
||||||
|
fs::write(&path, code)?;
|
||||||
|
|
||||||
|
// Try to format the output for debugging purposes.
|
||||||
|
// Doesn't matter if rustfmt is unavailable.
|
||||||
|
let _ = Command::new("rustfmt").arg(path).output();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
201
tools/packet_inspector/src/app.rs
Normal file
201
tools/packet_inspector/src/app.rs
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
use std::{
|
||||||
|
net::SocketAddr,
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
use egui_dock::{DockArea, NodeIndex, Style, Tree};
|
||||||
|
use packet_inspector::Proxy;
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
|
|
||||||
|
use crate::shared_state::{Event, SharedState};
|
||||||
|
|
||||||
|
mod connection;
|
||||||
|
mod filter;
|
||||||
|
mod hex_viewer;
|
||||||
|
mod packet_list;
|
||||||
|
mod text_viewer;
|
||||||
|
|
||||||
|
pub trait View {
|
||||||
|
fn ui(&mut self, ui: &mut egui::Ui, shared_state: &mut SharedState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Something to view
|
||||||
|
pub trait Tab: View {
|
||||||
|
fn new() -> Self
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
|
||||||
|
/// `&'static` so we can also use it as a key to store open/close state.
|
||||||
|
fn name(&self) -> &'static str;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TabViewer {
|
||||||
|
shared_state: Arc<RwLock<SharedState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl egui_dock::TabViewer for TabViewer {
|
||||||
|
type Tab = Box<dyn Tab>;
|
||||||
|
|
||||||
|
fn ui(&mut self, ui: &mut egui::Ui, tab: &mut Self::Tab) {
|
||||||
|
tab.ui(ui, &mut self.shared_state.write().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(&mut self, tab: &mut Self::Tab) -> egui::WidgetText {
|
||||||
|
tab.name().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_close(&mut self, _tab: &mut Self::Tab) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GuiApp {
|
||||||
|
tree: Tree<Box<dyn Tab>>,
|
||||||
|
shared_state: Arc<RwLock<SharedState>>,
|
||||||
|
tab_viewer: TabViewer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GuiApp {
|
||||||
|
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||||
|
let ctx = cc.egui_ctx.clone();
|
||||||
|
|
||||||
|
// Default Application Layout
|
||||||
|
let mut tree: Tree<Box<dyn Tab>> = Tree::new(vec![Box::new(connection::Connection::new())]);
|
||||||
|
|
||||||
|
let [a, b] = tree.split_right(
|
||||||
|
NodeIndex::root(),
|
||||||
|
0.3,
|
||||||
|
vec![Box::new(packet_list::PacketList::new())],
|
||||||
|
);
|
||||||
|
|
||||||
|
let [_, _] = tree.split_below(a, 0.25, vec![Box::new(filter::Filter::new())]);
|
||||||
|
let [_, _] = tree.split_below(
|
||||||
|
b,
|
||||||
|
0.5,
|
||||||
|
vec![
|
||||||
|
Box::new(hex_viewer::HexView::new()),
|
||||||
|
Box::new(text_viewer::TextView::new()),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Persistant Storage
|
||||||
|
let mut shared_state = SharedState::new(ctx);
|
||||||
|
|
||||||
|
if let Some(storage) = cc.storage {
|
||||||
|
if let Some(value) = eframe::get_value::<SharedState>(storage, eframe::APP_KEY) {
|
||||||
|
shared_state = value.merge(shared_state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let autostart = shared_state.autostart;
|
||||||
|
let shared_state = Arc::new(RwLock::new(shared_state));
|
||||||
|
|
||||||
|
// Event Handling
|
||||||
|
handle_events(shared_state.clone());
|
||||||
|
|
||||||
|
if autostart {
|
||||||
|
let state = shared_state.read().unwrap();
|
||||||
|
state.send_event(Event::StartListening);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consumer thread
|
||||||
|
|
||||||
|
// Tab Viewer
|
||||||
|
let tab_viewer = TabViewer {
|
||||||
|
shared_state: shared_state.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
shared_state,
|
||||||
|
tree,
|
||||||
|
tab_viewer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eframe::App for GuiApp {
|
||||||
|
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||||
|
eframe::set_value(
|
||||||
|
storage,
|
||||||
|
eframe::APP_KEY,
|
||||||
|
&*self.shared_state.read().unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
|
DockArea::new(&mut self.tree)
|
||||||
|
.show_add_buttons(false)
|
||||||
|
.show_add_popup(false)
|
||||||
|
.show_close_buttons(false)
|
||||||
|
.style(Style::from_egui(ctx.style().as_ref()))
|
||||||
|
.show(ctx, &mut self.tab_viewer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function is getting waaaay too complcated and messy
|
||||||
|
fn handle_events(state: Arc<RwLock<SharedState>>) {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut proxy_thread: Option<JoinHandle<_>> = None;
|
||||||
|
|
||||||
|
let receiver = state.write().unwrap().receiver.take().unwrap();
|
||||||
|
while let Ok(event) = receiver.recv_async().await {
|
||||||
|
match event {
|
||||||
|
Event::StartListening => {
|
||||||
|
let mut w_state = state.write().unwrap();
|
||||||
|
if w_state.is_listening {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(listener_addr) = w_state.listener_addr.parse::<SocketAddr>() else {
|
||||||
|
w_state.is_listening = false;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Ok(server_addr) = w_state.server_addr.parse::<SocketAddr>() else {
|
||||||
|
w_state.is_listening = false;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let state = state.clone();
|
||||||
|
|
||||||
|
proxy_thread = Some(tokio::spawn(async move {
|
||||||
|
let proxy = Proxy::new(listener_addr, server_addr);
|
||||||
|
let receiver = proxy.subscribe().await;
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
proxy.run().await?;
|
||||||
|
Ok::<(), anyhow::Error>(())
|
||||||
|
});
|
||||||
|
|
||||||
|
while let Ok(packet) = receiver.recv_async().await {
|
||||||
|
let state = state.read().unwrap();
|
||||||
|
state.packets.write().unwrap().push(packet);
|
||||||
|
state.send_event(Event::PacketReceived);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<(), anyhow::Error>(())
|
||||||
|
}));
|
||||||
|
|
||||||
|
w_state.is_listening = true;
|
||||||
|
}
|
||||||
|
Event::StopListening => {
|
||||||
|
let mut state = state.write().unwrap();
|
||||||
|
if !state.is_listening {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(proxy_thread) = proxy_thread.take() {
|
||||||
|
proxy_thread.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
state.is_listening = false;
|
||||||
|
}
|
||||||
|
Event::PacketReceived => {
|
||||||
|
// Refresh UI
|
||||||
|
if let Some(ctx) = &state.read().unwrap().ctx {
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
44
tools/packet_inspector/src/app/connection.rs
Normal file
44
tools/packet_inspector/src/app/connection.rs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
use crate::shared_state::Event;
|
||||||
|
|
||||||
|
use super::{SharedState, Tab, View};
|
||||||
|
|
||||||
|
pub struct Connection {}
|
||||||
|
|
||||||
|
impl Tab for Connection {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"Connection"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for Connection {
|
||||||
|
fn ui(&mut self, ui: &mut egui::Ui, state: &mut SharedState) {
|
||||||
|
if state.is_listening {
|
||||||
|
ui.label("Listener Address");
|
||||||
|
ui.text_edit_singleline(&mut state.listener_addr.clone());
|
||||||
|
ui.label("Server Address");
|
||||||
|
ui.text_edit_singleline(&mut state.server_addr.clone());
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if ui.button("Stop Listening").clicked() {
|
||||||
|
state.send_event(Event::StopListening);
|
||||||
|
}
|
||||||
|
ui.checkbox(&mut state.autostart, "Autostart");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ui.label("Listener Address");
|
||||||
|
ui.text_edit_singleline(&mut state.listener_addr);
|
||||||
|
ui.label("Server Address");
|
||||||
|
ui.text_edit_singleline(&mut state.server_addr);
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if ui.button("Start Listening").clicked() {
|
||||||
|
state.send_event(Event::StartListening);
|
||||||
|
}
|
||||||
|
ui.checkbox(&mut state.autostart, "Autostart");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
132
tools/packet_inspector/src/app/filter.rs
Normal file
132
tools/packet_inspector/src/app/filter.rs
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
use egui::{RichText, Ui, Widget};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use packet_inspector::PacketState;
|
||||||
|
|
||||||
|
use crate::tri_checkbox::{TriCheckbox, TriCheckboxState};
|
||||||
|
|
||||||
|
use super::{SharedState, Tab, View};
|
||||||
|
|
||||||
|
pub struct Filter {}
|
||||||
|
|
||||||
|
impl Tab for Filter {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"Filters"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for Filter {
|
||||||
|
fn ui(&mut self, ui: &mut egui::Ui, state: &mut SharedState) {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Search:");
|
||||||
|
if ui.button("x").clicked() {
|
||||||
|
state.packet_search.clear();
|
||||||
|
}
|
||||||
|
ui.text_edit_singleline(&mut state.packet_search);
|
||||||
|
});
|
||||||
|
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.auto_shrink([false, false])
|
||||||
|
.stick_to_bottom(false)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
if draw_packet_list(ui, state, PacketState::Handshaking) > 0 {
|
||||||
|
ui.separator();
|
||||||
|
}
|
||||||
|
if draw_packet_list(ui, state, PacketState::Status) > 0 {
|
||||||
|
ui.separator();
|
||||||
|
}
|
||||||
|
if draw_packet_list(ui, state, PacketState::Login) > 0 {
|
||||||
|
ui.separator();
|
||||||
|
}
|
||||||
|
draw_packet_list(ui, state, PacketState::Play);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_checkbox_state(state: &SharedState, packet_state: PacketState) -> TriCheckboxState {
|
||||||
|
let mut p_enabled = 0;
|
||||||
|
let mut disabled = 0;
|
||||||
|
for (_, enabled) in state
|
||||||
|
.packet_filter
|
||||||
|
.iter()
|
||||||
|
.filter(|(p, _)| p.state == packet_state)
|
||||||
|
{
|
||||||
|
if *enabled {
|
||||||
|
p_enabled += 1;
|
||||||
|
} else {
|
||||||
|
disabled += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p_enabled > 0 && disabled == 0 {
|
||||||
|
TriCheckboxState::Enabled
|
||||||
|
} else if p_enabled > 0 && disabled > 0 {
|
||||||
|
TriCheckboxState::Partial
|
||||||
|
} else {
|
||||||
|
TriCheckboxState::Disabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_packet_list(ui: &mut Ui, state: &mut SharedState, packet_state: PacketState) -> usize {
|
||||||
|
let title = match packet_state {
|
||||||
|
PacketState::Handshaking => "Handshaking",
|
||||||
|
PacketState::Status => "Status",
|
||||||
|
PacketState::Login => "Login",
|
||||||
|
PacketState::Play => "Play",
|
||||||
|
};
|
||||||
|
|
||||||
|
let search = state.packet_search.to_lowercase();
|
||||||
|
|
||||||
|
let count = state
|
||||||
|
.packet_filter
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|(p, _)| p.state == packet_state && p.name.to_lowercase().contains(&search))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
let count_enabled = state
|
||||||
|
.packet_filter
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|(p, enabled)| {
|
||||||
|
p.state == packet_state && p.name.to_lowercase().contains(&search) && **enabled
|
||||||
|
})
|
||||||
|
.count();
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut checkbox = get_checkbox_state(state, packet_state);
|
||||||
|
if TriCheckbox::new(&mut checkbox, RichText::new(title).heading().strong())
|
||||||
|
.ui(ui)
|
||||||
|
.changed()
|
||||||
|
{
|
||||||
|
for (_, enabled) in state
|
||||||
|
.packet_filter
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|(p, _)| p.state == packet_state && p.name.to_lowercase().contains(&search))
|
||||||
|
{
|
||||||
|
if count == count_enabled || count_enabled == 0 {
|
||||||
|
*enabled = !*enabled;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
*enabled = (count / 2) <= count_enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (p, enabled) in state
|
||||||
|
.packet_filter
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|(p, _)| p.state == packet_state && p.name.to_lowercase().contains(&search))
|
||||||
|
.sorted_by(|(a, _), (b, _)| {
|
||||||
|
a.id.cmp(&b.id)
|
||||||
|
.then((a.side as usize).cmp(&(b.side as usize)))
|
||||||
|
})
|
||||||
|
{
|
||||||
|
ui.checkbox(enabled, format!("[0x{:0>2X}] {}", p.id, p.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
69
tools/packet_inspector/src/app/hex_viewer.rs
Normal file
69
tools/packet_inspector/src/app/hex_viewer.rs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
use egui::Color32;
|
||||||
|
|
||||||
|
use super::{SharedState, Tab, View};
|
||||||
|
|
||||||
|
pub struct HexView {}
|
||||||
|
|
||||||
|
impl Tab for HexView {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"Hex Viewer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for HexView {
|
||||||
|
fn ui(&mut self, ui: &mut egui::Ui, state: &mut SharedState) {
|
||||||
|
let mut buf = [0u8; 16];
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
let packets = state.packets.read().unwrap();
|
||||||
|
let Some(packet_index) = state.selected_packet else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let bytes = &packets[packet_index].data.as_ref().unwrap();
|
||||||
|
let mut file = &(*bytes).clone()[..];
|
||||||
|
|
||||||
|
egui::Grid::new("hex_grid")
|
||||||
|
.spacing([4.0, 1.5])
|
||||||
|
.striped(true)
|
||||||
|
.min_col_width(0.0)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.label(" ");
|
||||||
|
for i in 0..16 {
|
||||||
|
ui.label(format!("{:02X}", i));
|
||||||
|
}
|
||||||
|
ui.end_row();
|
||||||
|
loop {
|
||||||
|
let bytes_read = file.read(&mut buf).unwrap();
|
||||||
|
if bytes_read == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.label(format!("{:08X}", count));
|
||||||
|
for b in buf.iter().take(bytes_read) {
|
||||||
|
ui.colored_label(Color32::from_rgb(255, 255, 255), format!("{:02X}", b));
|
||||||
|
}
|
||||||
|
for _ in 0..16 - bytes_read {
|
||||||
|
ui.label(" ");
|
||||||
|
}
|
||||||
|
ui.label(" ");
|
||||||
|
for b in buf.iter().take(bytes_read) {
|
||||||
|
if *b >= 0x20 && *b <= 0x7e {
|
||||||
|
ui.label(format!("{}", *b as char));
|
||||||
|
} else {
|
||||||
|
ui.label(".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.end_row();
|
||||||
|
count += bytes_read;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
264
tools/packet_inspector/src/app/packet_list.rs
Normal file
264
tools/packet_inspector/src/app/packet_list.rs
Normal file
|
@ -0,0 +1,264 @@
|
||||||
|
use eframe::epaint::{PathShape, RectShape};
|
||||||
|
use egui::{
|
||||||
|
Color32, Pos2, Rect, Response, Rgba, Rounding, Sense, Shape, Stroke, TextStyle, Ui, Vec2,
|
||||||
|
WidgetText,
|
||||||
|
};
|
||||||
|
|
||||||
|
use packet_inspector::{Packet, PacketSide};
|
||||||
|
|
||||||
|
use super::{SharedState, Tab, View};
|
||||||
|
|
||||||
|
pub struct PacketList {}
|
||||||
|
|
||||||
|
impl Tab for PacketList {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"Packets"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for PacketList {
|
||||||
|
fn ui(&mut self, ui: &mut egui::Ui, state: &mut SharedState) {
|
||||||
|
handle_keyboard_input(state, ui);
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.heading("Packets");
|
||||||
|
draw_packet_counter(state, ui);
|
||||||
|
draw_clear_button(state, ui);
|
||||||
|
});
|
||||||
|
|
||||||
|
draw_packet_list(state, ui);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_keyboard_input(state: &mut SharedState, ui: &mut Ui) {
|
||||||
|
if ui.input(|i| i.key_pressed(egui::Key::ArrowUp)) {
|
||||||
|
// select previous packet
|
||||||
|
let index = state.selected_packet.unwrap_or(1);
|
||||||
|
|
||||||
|
let packets = state.packets.read().unwrap();
|
||||||
|
let filtered_packets = packets
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(i, p)| *i < index && state.packet_filter.get(p).unwrap_or(true))
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if let Some(&prev_index) = filtered_packets.last() {
|
||||||
|
state.selected_packet = Some(prev_index);
|
||||||
|
state.update_scroll = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.input(|i| i.key_pressed(egui::Key::ArrowDown)) {
|
||||||
|
// select next packet
|
||||||
|
let index = state.selected_packet.unwrap_or(0);
|
||||||
|
|
||||||
|
let packets = state.packets.read().unwrap();
|
||||||
|
let filtered_packets = packets
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(i, p)| *i > index && state.packet_filter.get(p).unwrap_or(true))
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if let Some(&next_index) = filtered_packets.first() {
|
||||||
|
state.selected_packet = Some(next_index);
|
||||||
|
state.update_scroll = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_packet_counter(state: &mut SharedState, ui: &mut Ui) {
|
||||||
|
let packets = state.packets.read().unwrap();
|
||||||
|
let length = packets.len();
|
||||||
|
|
||||||
|
let filtered_packets = packets
|
||||||
|
.iter()
|
||||||
|
.filter(|p| state.packet_filter.get(p).unwrap_or(true))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
ui.label(format!("({}/{})", filtered_packets, length));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_clear_button(state: &mut SharedState, ui: &mut Ui) {
|
||||||
|
if ui.button("Clear").clicked() {
|
||||||
|
state.selected_packet = None;
|
||||||
|
state.packets.write().unwrap().clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_packet_list(state: &mut SharedState, ui: &mut Ui) {
|
||||||
|
let packets = state.packets.read().unwrap();
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.auto_shrink([false, false])
|
||||||
|
.stick_to_bottom(!state.update_scroll)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
for (i, packet) in packets.iter().enumerate() {
|
||||||
|
if let Some(filtered) = state.packet_filter.get(packet) {
|
||||||
|
if !filtered {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let selected = {
|
||||||
|
if let Some(selected) = state.selected_packet {
|
||||||
|
selected == i
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let widget = draw_packet_widget(ui, packet, selected);
|
||||||
|
|
||||||
|
if state.update_scroll && state.selected_packet == Some(i) {
|
||||||
|
state.update_scroll = false;
|
||||||
|
ui.scroll_to_rect(widget.rect, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
if widget.clicked() {
|
||||||
|
state.selected_packet = Some(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_packet_widget(ui: &mut Ui, packet: &Packet, selected: bool) -> Response {
|
||||||
|
let (mut rect, response) = ui.allocate_at_least(
|
||||||
|
Vec2 {
|
||||||
|
x: ui.available_width(),
|
||||||
|
y: 24.0,
|
||||||
|
},
|
||||||
|
Sense::click(),
|
||||||
|
); // this should give me a new rect inside the scroll area... no?
|
||||||
|
|
||||||
|
let fill = match selected /*packet.selected*/ {
|
||||||
|
true => Rgba::from_rgba_premultiplied(0.3, 0.3, 0.3, 0.4),
|
||||||
|
false => Rgba::from_rgba_premultiplied(0.0, 0.0, 0.0, 0.0),
|
||||||
|
};
|
||||||
|
|
||||||
|
let text_color: Color32 = match selected /*packet.selected*/ {
|
||||||
|
true => Rgba::from_rgba_premultiplied(0.0, 0.0, 0.0, 1.0).into(),
|
||||||
|
false => ui.visuals().strong_text_color(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if ui.is_rect_visible(rect) {
|
||||||
|
ui.painter().add(Shape::Rect(RectShape {
|
||||||
|
rect,
|
||||||
|
rounding: Rounding::none(),
|
||||||
|
fill: fill.into(),
|
||||||
|
stroke: Stroke::new(1.0, Rgba::BLACK),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let shape = get_triangle(packet.side, &rect);
|
||||||
|
ui.painter().add(Shape::Path(shape));
|
||||||
|
|
||||||
|
let identifier: WidgetText = format!("0x{:0>2X?}", packet.id).into();
|
||||||
|
|
||||||
|
let identifier =
|
||||||
|
identifier.into_galley(ui, Some(false), rect.width() - 21.0, TextStyle::Button);
|
||||||
|
|
||||||
|
let label: WidgetText = packet.name.into();
|
||||||
|
let label = label.into_galley(ui, Some(false), rect.width() - 60.0, TextStyle::Button);
|
||||||
|
|
||||||
|
let timestamp: WidgetText = systemtime_strftime(packet.timestamp.unwrap()).into();
|
||||||
|
let timestamp =
|
||||||
|
timestamp.into_galley(ui, Some(false), rect.width() - 60.0, TextStyle::Button);
|
||||||
|
|
||||||
|
identifier.paint_with_fallback_color(
|
||||||
|
ui.painter(),
|
||||||
|
Pos2 {
|
||||||
|
x: rect.left() + 21.0,
|
||||||
|
y: rect.top() + 6.0,
|
||||||
|
},
|
||||||
|
ui.visuals().weak_text_color(),
|
||||||
|
);
|
||||||
|
|
||||||
|
rect.set_width(rect.width() - 5.0);
|
||||||
|
|
||||||
|
let label_width = label.size().x + 50.0;
|
||||||
|
|
||||||
|
label.paint_with_fallback_color(
|
||||||
|
&ui.painter().with_clip_rect(rect),
|
||||||
|
Pos2 {
|
||||||
|
x: rect.left() + 55.0,
|
||||||
|
y: rect.top() + 6.0,
|
||||||
|
},
|
||||||
|
text_color,
|
||||||
|
);
|
||||||
|
|
||||||
|
timestamp.paint_with_fallback_color(
|
||||||
|
&ui.painter().with_clip_rect(rect),
|
||||||
|
Pos2 {
|
||||||
|
x: rect.left() + label_width + 8.0,
|
||||||
|
y: rect.top() + 6.0,
|
||||||
|
},
|
||||||
|
ui.visuals().weak_text_color(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_triangle(direction: PacketSide, outer_rect: &Rect) -> PathShape {
|
||||||
|
let rect = Rect::from_min_size(
|
||||||
|
Pos2 {
|
||||||
|
x: outer_rect.left() + 6.0,
|
||||||
|
y: outer_rect.top() + 8.0,
|
||||||
|
},
|
||||||
|
Vec2 { x: 8.0, y: 8.0 },
|
||||||
|
);
|
||||||
|
|
||||||
|
let color = match direction {
|
||||||
|
PacketSide::Clientbound => Rgba::from_rgb(255.0, 0.0, 0.0),
|
||||||
|
PacketSide::Serverbound => Rgba::from_rgb(0.0, 255.0, 0.0),
|
||||||
|
};
|
||||||
|
|
||||||
|
let points = match direction {
|
||||||
|
PacketSide::Clientbound => vec![
|
||||||
|
Pos2 {
|
||||||
|
x: rect.left() + (rect.width() / 2.0),
|
||||||
|
y: rect.top() + rect.height(),
|
||||||
|
},
|
||||||
|
Pos2 {
|
||||||
|
x: rect.left() + 0.0,
|
||||||
|
y: rect.top(),
|
||||||
|
},
|
||||||
|
Pos2 {
|
||||||
|
x: rect.left() + rect.width(),
|
||||||
|
y: rect.top(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
PacketSide::Serverbound => vec![
|
||||||
|
Pos2 {
|
||||||
|
x: rect.left() + (rect.width() / 2.0),
|
||||||
|
y: rect.top() + 0.0,
|
||||||
|
},
|
||||||
|
Pos2 {
|
||||||
|
x: rect.left() + 0.0,
|
||||||
|
y: rect.top() + rect.height(),
|
||||||
|
},
|
||||||
|
Pos2 {
|
||||||
|
x: rect.left() + rect.width(),
|
||||||
|
y: rect.top() + rect.height(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut shape = PathShape::closed_line(points, Stroke::new(2.0, color));
|
||||||
|
shape.fill = color.into();
|
||||||
|
|
||||||
|
shape
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn systemtime_strftime(odt: time::OffsetDateTime) -> String {
|
||||||
|
let hour = odt.hour();
|
||||||
|
let minute = odt.minute();
|
||||||
|
let second = odt.second();
|
||||||
|
let millis = odt.millisecond();
|
||||||
|
|
||||||
|
format!("{hour:0>2}:{minute:0>2}:{second:0>2}.{millis:0>4}")
|
||||||
|
}
|
353
tools/packet_inspector/src/app/text_viewer.rs
Normal file
353
tools/packet_inspector/src/app/text_viewer.rs
Normal file
|
@ -0,0 +1,353 @@
|
||||||
|
use super::{SharedState, Tab, View};
|
||||||
|
|
||||||
|
mod utils {
|
||||||
|
use packet_inspector::Packet as ProxyPacket;
|
||||||
|
use packet_inspector::{PacketSide, PacketState};
|
||||||
|
use valence::protocol::{Decode, Packet};
|
||||||
|
|
||||||
|
use valence::advancement::packet::*;
|
||||||
|
use valence::client::action::*;
|
||||||
|
use valence::client::command::*;
|
||||||
|
use valence::client::custom_payload::*;
|
||||||
|
use valence::client::hand_swing::*;
|
||||||
|
use valence::client::interact_block::*;
|
||||||
|
use valence::client::interact_entity::*;
|
||||||
|
use valence::client::interact_item::*;
|
||||||
|
use valence::client::keepalive::*;
|
||||||
|
use valence::client::movement::*;
|
||||||
|
use valence::client::packet::structure_block::*;
|
||||||
|
use valence::client::packet::*;
|
||||||
|
use valence::client::resource_pack::*;
|
||||||
|
use valence::client::settings::*;
|
||||||
|
use valence::client::status::*;
|
||||||
|
use valence::client::teleport::*;
|
||||||
|
use valence::client::title::*;
|
||||||
|
use valence::entity::packet::*;
|
||||||
|
use valence::instance::packet::*;
|
||||||
|
use valence::inventory::packet::synchronize_recipes::*;
|
||||||
|
use valence::inventory::packet::*;
|
||||||
|
use valence::network::packet::*;
|
||||||
|
use valence::particle::*;
|
||||||
|
use valence::player_list::packet::*;
|
||||||
|
use valence::protocol::packet::boss_bar::*;
|
||||||
|
use valence::protocol::packet::chat::*;
|
||||||
|
use valence::protocol::packet::command::*;
|
||||||
|
use valence::protocol::packet::map::*;
|
||||||
|
use valence::protocol::packet::scoreboard::*;
|
||||||
|
use valence::protocol::packet::sound::*;
|
||||||
|
use valence::registry::tags::*;
|
||||||
|
use valence::world_border::packet::*;
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/packet_to_string.rs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TextView {
|
||||||
|
last_packet_id: Option<usize>,
|
||||||
|
packet_str: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tab for TextView {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
last_packet_id: None,
|
||||||
|
packet_str: "".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"Text Viewer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for TextView {
|
||||||
|
fn ui(&mut self, ui: &mut egui::Ui, state: &mut SharedState) {
|
||||||
|
let packets = state.packets.read().unwrap();
|
||||||
|
let Some(packet_index) = state.selected_packet else {
|
||||||
|
self.last_packet_id = None;
|
||||||
|
self.packet_str = "".to_string();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.last_packet_id != Some(packet_index) {
|
||||||
|
self.last_packet_id = Some(packet_index);
|
||||||
|
self.packet_str = utils::packet_to_string(&packets[packet_index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
code_view_ui(ui, &self.packet_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// From: https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/syntax_highlighting.rs
|
||||||
|
|
||||||
|
use egui::text::LayoutJob;
|
||||||
|
|
||||||
|
/// View some code with syntax highlighting and selection.
|
||||||
|
pub fn code_view_ui(ui: &mut egui::Ui, mut code: &str) {
|
||||||
|
let language = "rs";
|
||||||
|
let theme = CodeTheme::from_memory(ui.ctx());
|
||||||
|
|
||||||
|
let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
|
||||||
|
let mut layout_job = highlight(ui.ctx(), &theme, string, language);
|
||||||
|
layout_job.wrap.max_width = wrap_width; // no wrapping
|
||||||
|
ui.fonts(|f| f.layout_job(layout_job))
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.add(
|
||||||
|
egui::TextEdit::multiline(&mut code)
|
||||||
|
.font(egui::TextStyle::Monospace) // for cursor height
|
||||||
|
.code_editor()
|
||||||
|
.desired_width(ui.available_width())
|
||||||
|
.desired_rows(24)
|
||||||
|
.lock_focus(true)
|
||||||
|
.layouter(&mut layouter),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Memoized Code highlighting
|
||||||
|
pub fn highlight(ctx: &egui::Context, theme: &CodeTheme, code: &str, language: &str) -> LayoutJob {
|
||||||
|
impl egui::util::cache::ComputerMut<(&CodeTheme, &str, &str), LayoutJob> for Highlighter {
|
||||||
|
fn compute(&mut self, (theme, code, lang): (&CodeTheme, &str, &str)) -> LayoutJob {
|
||||||
|
self.highlight(theme, code, lang)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type HighlightCache = egui::util::cache::FrameCache<LayoutJob, Highlighter>;
|
||||||
|
|
||||||
|
ctx.memory_mut(|mem| {
|
||||||
|
mem.caches
|
||||||
|
.cache::<HighlightCache>()
|
||||||
|
.get((theme, code, language))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[allow(unused)]
|
||||||
|
enum SyntectTheme {
|
||||||
|
Base16EightiesDark,
|
||||||
|
Base16MochaDark,
|
||||||
|
Base16OceanDark,
|
||||||
|
Base16OceanLight,
|
||||||
|
InspiredGitHub,
|
||||||
|
SolarizedDark,
|
||||||
|
SolarizedLight,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
impl SyntectTheme {
|
||||||
|
fn all() -> impl ExactSizeIterator<Item = Self> {
|
||||||
|
[
|
||||||
|
Self::Base16EightiesDark,
|
||||||
|
Self::Base16MochaDark,
|
||||||
|
Self::Base16OceanDark,
|
||||||
|
Self::Base16OceanLight,
|
||||||
|
Self::InspiredGitHub,
|
||||||
|
Self::SolarizedDark,
|
||||||
|
Self::SolarizedLight,
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Base16EightiesDark => "Base16 Eighties (dark)",
|
||||||
|
Self::Base16MochaDark => "Base16 Mocha (dark)",
|
||||||
|
Self::Base16OceanDark => "Base16 Ocean (dark)",
|
||||||
|
Self::Base16OceanLight => "Base16 Ocean (light)",
|
||||||
|
Self::InspiredGitHub => "InspiredGitHub (light)",
|
||||||
|
Self::SolarizedDark => "Solarized (dark)",
|
||||||
|
Self::SolarizedLight => "Solarized (light)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn syntect_key_name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Base16EightiesDark => "base16-eighties.dark",
|
||||||
|
Self::Base16MochaDark => "base16-mocha.dark",
|
||||||
|
Self::Base16OceanDark => "base16-ocean.dark",
|
||||||
|
Self::Base16OceanLight => "base16-ocean.light",
|
||||||
|
Self::InspiredGitHub => "InspiredGitHub",
|
||||||
|
Self::SolarizedDark => "Solarized (dark)",
|
||||||
|
Self::SolarizedLight => "Solarized (light)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_dark(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Base16EightiesDark
|
||||||
|
| Self::Base16MochaDark
|
||||||
|
| Self::Base16OceanDark
|
||||||
|
| Self::SolarizedDark => true,
|
||||||
|
|
||||||
|
Self::Base16OceanLight | Self::InspiredGitHub | Self::SolarizedLight => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct CodeTheme {
|
||||||
|
dark_mode: bool,
|
||||||
|
|
||||||
|
syntect_theme: SyntectTheme,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CodeTheme {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::dark()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
impl CodeTheme {
|
||||||
|
pub fn from_style(style: &egui::Style) -> Self {
|
||||||
|
if style.visuals.dark_mode {
|
||||||
|
Self::dark()
|
||||||
|
} else {
|
||||||
|
Self::light()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_memory(ctx: &egui::Context) -> Self {
|
||||||
|
if ctx.style().visuals.dark_mode {
|
||||||
|
ctx.data_mut(|d| {
|
||||||
|
d.get_persisted(egui::Id::new("dark"))
|
||||||
|
.unwrap_or_else(CodeTheme::dark)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
ctx.data_mut(|d| {
|
||||||
|
d.get_persisted(egui::Id::new("light"))
|
||||||
|
.unwrap_or_else(CodeTheme::light)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn store_in_memory(self, ctx: &egui::Context) {
|
||||||
|
if self.dark_mode {
|
||||||
|
ctx.data_mut(|d| d.insert_persisted(egui::Id::new("dark"), self));
|
||||||
|
} else {
|
||||||
|
ctx.data_mut(|d| d.insert_persisted(egui::Id::new("light"), self));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
impl CodeTheme {
|
||||||
|
pub fn dark() -> Self {
|
||||||
|
Self {
|
||||||
|
dark_mode: true,
|
||||||
|
syntect_theme: SyntectTheme::SolarizedDark,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn light() -> Self {
|
||||||
|
Self {
|
||||||
|
dark_mode: false,
|
||||||
|
syntect_theme: SyntectTheme::SolarizedLight,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
egui::widgets::global_dark_light_mode_buttons(ui);
|
||||||
|
|
||||||
|
for theme in SyntectTheme::all() {
|
||||||
|
if theme.is_dark() == self.dark_mode {
|
||||||
|
ui.radio_value(&mut self.syntect_theme, theme, theme.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
struct Highlighter {
|
||||||
|
ps: syntect::parsing::SyntaxSet,
|
||||||
|
ts: syntect::highlighting::ThemeSet,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Highlighter {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
ps: syntect::parsing::SyntaxSet::load_defaults_newlines(),
|
||||||
|
ts: syntect::highlighting::ThemeSet::load_defaults(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Highlighter {
|
||||||
|
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
||||||
|
fn highlight(&self, theme: &CodeTheme, code: &str, lang: &str) -> LayoutJob {
|
||||||
|
self.highlight_impl(theme, code, lang).unwrap_or_else(|| {
|
||||||
|
// Fallback:
|
||||||
|
LayoutJob::simple(
|
||||||
|
code.into(),
|
||||||
|
egui::FontId::monospace(12.0),
|
||||||
|
if theme.dark_mode {
|
||||||
|
egui::Color32::LIGHT_GRAY
|
||||||
|
} else {
|
||||||
|
egui::Color32::DARK_GRAY
|
||||||
|
},
|
||||||
|
f32::INFINITY,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn highlight_impl(&self, theme: &CodeTheme, text: &str, language: &str) -> Option<LayoutJob> {
|
||||||
|
use syntect::easy::HighlightLines;
|
||||||
|
use syntect::highlighting::FontStyle;
|
||||||
|
use syntect::util::LinesWithEndings;
|
||||||
|
|
||||||
|
let syntax = self
|
||||||
|
.ps
|
||||||
|
.find_syntax_by_name(language)
|
||||||
|
.or_else(|| self.ps.find_syntax_by_extension(language))?;
|
||||||
|
|
||||||
|
let theme = theme.syntect_theme.syntect_key_name();
|
||||||
|
let mut h = HighlightLines::new(syntax, &self.ts.themes[theme]);
|
||||||
|
|
||||||
|
use egui::text::{LayoutSection, TextFormat};
|
||||||
|
|
||||||
|
let mut job = LayoutJob {
|
||||||
|
text: text.into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
for line in LinesWithEndings::from(text) {
|
||||||
|
for (style, range) in h.highlight_line(line, &self.ps).ok()? {
|
||||||
|
let fg = style.foreground;
|
||||||
|
let text_color = egui::Color32::from_rgb(fg.r, fg.g, fg.b);
|
||||||
|
let italics = style.font_style.contains(FontStyle::ITALIC);
|
||||||
|
let underline = style.font_style.contains(FontStyle::ITALIC);
|
||||||
|
let underline = if underline {
|
||||||
|
egui::Stroke::new(1.0, text_color)
|
||||||
|
} else {
|
||||||
|
egui::Stroke::NONE
|
||||||
|
};
|
||||||
|
job.sections.push(LayoutSection {
|
||||||
|
leading_space: 0.0,
|
||||||
|
byte_range: as_byte_range(text, range),
|
||||||
|
format: TextFormat {
|
||||||
|
font_id: egui::FontId::monospace(12.0),
|
||||||
|
color: text_color,
|
||||||
|
italics,
|
||||||
|
underline,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(job)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_byte_range(whole: &str, range: &str) -> std::ops::Range<usize> {
|
||||||
|
let whole_start = whole.as_ptr() as usize;
|
||||||
|
let range_start = range.as_ptr() as usize;
|
||||||
|
assert!(whole_start <= range_start);
|
||||||
|
assert!(range_start + range.len() <= whole_start + whole.len());
|
||||||
|
let offset = range_start - whole_start;
|
||||||
|
offset..(offset + range.len())
|
||||||
|
}
|
|
@ -1,131 +0,0 @@
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use directories::ProjectDirs;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::MetaPacket;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct ApplicationConfig {
|
|
||||||
server_addr: SocketAddr,
|
|
||||||
client_addr: SocketAddr,
|
|
||||||
max_connections: Option<usize>,
|
|
||||||
filter: Option<String>,
|
|
||||||
selected_packets: Option<BTreeMap<MetaPacket, bool>>,
|
|
||||||
|
|
||||||
#[serde(skip_serializing, skip_deserializing)]
|
|
||||||
must_save: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ApplicationConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
server_addr: "127.0.0.1:25565".parse().unwrap(),
|
|
||||||
client_addr: "127.0.0.1:25566".parse().unwrap(),
|
|
||||||
max_connections: None,
|
|
||||||
filter: None,
|
|
||||||
selected_packets: None,
|
|
||||||
must_save: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ApplicationConfig {
|
|
||||||
pub fn load() -> Result<ApplicationConfig, Box<dyn std::error::Error>> {
|
|
||||||
let config_dir = get_or_create_project_dirs()?;
|
|
||||||
|
|
||||||
let config_file = config_dir.join("config.toml");
|
|
||||||
|
|
||||||
if config_file.exists() {
|
|
||||||
let config = std::fs::read_to_string(config_file)?;
|
|
||||||
|
|
||||||
toml::from_str(&config).map_err(|e| e.into())
|
|
||||||
} else {
|
|
||||||
let config = toml::to_string(&ApplicationConfig::default()).unwrap();
|
|
||||||
std::fs::write(config_file, config)?;
|
|
||||||
Ok(ApplicationConfig::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn server_addr(&self) -> SocketAddr {
|
|
||||||
self.server_addr
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn client_addr(&self) -> SocketAddr {
|
|
||||||
self.client_addr
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn max_connections(&self) -> Option<usize> {
|
|
||||||
self.max_connections
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn filter(&self) -> &Option<String> {
|
|
||||||
&self.filter
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn selected_packets(&self) -> &Option<BTreeMap<MetaPacket, bool>> {
|
|
||||||
&self.selected_packets
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_server_addr(&mut self, addr: SocketAddr) {
|
|
||||||
self.must_save = true;
|
|
||||||
self.server_addr = addr;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_client_addr(&mut self, addr: SocketAddr) {
|
|
||||||
self.must_save = true;
|
|
||||||
self.client_addr = addr;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_max_connections(&mut self, max: Option<usize>) {
|
|
||||||
self.must_save = true;
|
|
||||||
self.max_connections = max;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_filter(&mut self, filter: Option<String>) {
|
|
||||||
self.must_save = true;
|
|
||||||
self.filter = filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_selected_packets(&mut self, packets: BTreeMap<MetaPacket, bool>) {
|
|
||||||
self.must_save = true;
|
|
||||||
self.selected_packets = Some(packets);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
if !self.must_save {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
self.must_save = false;
|
|
||||||
|
|
||||||
let config_dir = match get_or_create_project_dirs() {
|
|
||||||
Ok(dir) => dir,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Could not find config directory: {}", e);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let config_file = config_dir.join("config.toml");
|
|
||||||
|
|
||||||
let config = toml::to_string(&self).unwrap();
|
|
||||||
std::fs::write(config_file, config).unwrap();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_or_create_project_dirs() -> Result<PathBuf, Box<dyn std::error::Error>> {
|
|
||||||
if let Some(proj_dirs) = ProjectDirs::from("com", "valence", "inspector") {
|
|
||||||
// check if the directory exists, if not create it
|
|
||||||
if !proj_dirs.config_dir().exists() {
|
|
||||||
std::fs::create_dir_all(proj_dirs.config_dir())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(proj_dirs.config_dir().to_owned())
|
|
||||||
} else {
|
|
||||||
Err("Could not find project directories".into())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,356 +0,0 @@
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
|
||||||
use std::sync::RwLock;
|
|
||||||
|
|
||||||
use owo_colors::{OwoColorize, Style};
|
|
||||||
use regex::Regex;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use time::OffsetDateTime;
|
|
||||||
use valence::network::packet::{
|
|
||||||
HandshakeC2s, LoginHelloC2s, LoginKeyC2s, LoginSuccessS2c, QueryPingC2s, QueryPongS2c,
|
|
||||||
QueryRequestC2s, QueryResponseS2c,
|
|
||||||
};
|
|
||||||
use valence::protocol::decode::PacketDecoder;
|
|
||||||
|
|
||||||
use crate::packet_groups::{C2sPlayPacket, S2cLoginPacket, S2cPlayPacket};
|
|
||||||
use crate::packet_widget::{systemtime_strftime, PacketDirection};
|
|
||||||
use crate::MetaPacket;
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub enum Stage {
|
|
||||||
HandshakeC2s,
|
|
||||||
QueryRequestC2s,
|
|
||||||
QueryResponseS2c,
|
|
||||||
QueryPingC2s,
|
|
||||||
QueryPongS2c,
|
|
||||||
LoginHelloC2s,
|
|
||||||
S2cLoginPacket,
|
|
||||||
LoginKeyC2s,
|
|
||||||
LoginSuccessS2c,
|
|
||||||
C2sPlayPacket,
|
|
||||||
S2cPlayPacket,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Stage> for usize {
|
|
||||||
fn from(stage: Stage) -> Self {
|
|
||||||
match stage {
|
|
||||||
Stage::HandshakeC2s => 0,
|
|
||||||
Stage::QueryRequestC2s => 1,
|
|
||||||
Stage::QueryResponseS2c => 2,
|
|
||||||
Stage::QueryPingC2s => 3,
|
|
||||||
Stage::QueryPongS2c => 4,
|
|
||||||
Stage::LoginHelloC2s => 5,
|
|
||||||
Stage::S2cLoginPacket => 6,
|
|
||||||
Stage::LoginKeyC2s => 7,
|
|
||||||
Stage::LoginSuccessS2c => 8,
|
|
||||||
Stage::C2sPlayPacket => 9,
|
|
||||||
Stage::S2cPlayPacket => 10,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<usize> for Stage {
|
|
||||||
type Error = anyhow::Error;
|
|
||||||
|
|
||||||
fn try_from(value: usize) -> anyhow::Result<Self> {
|
|
||||||
match value {
|
|
||||||
0 => Ok(Stage::HandshakeC2s),
|
|
||||||
1 => Ok(Stage::QueryRequestC2s),
|
|
||||||
2 => Ok(Stage::QueryResponseS2c),
|
|
||||||
3 => Ok(Stage::QueryPingC2s),
|
|
||||||
4 => Ok(Stage::QueryPongS2c),
|
|
||||||
5 => Ok(Stage::LoginHelloC2s),
|
|
||||||
6 => Ok(Stage::S2cLoginPacket),
|
|
||||||
7 => Ok(Stage::LoginKeyC2s),
|
|
||||||
8 => Ok(Stage::LoginSuccessS2c),
|
|
||||||
9 => Ok(Stage::C2sPlayPacket),
|
|
||||||
10 => Ok(Stage::S2cPlayPacket),
|
|
||||||
_ => Err(anyhow::anyhow!("Invalid stage")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Packet {
|
|
||||||
pub(crate) id: usize,
|
|
||||||
pub(crate) direction: PacketDirection,
|
|
||||||
pub(crate) selected: bool,
|
|
||||||
pub(crate) compression_threshold: Option<u32>,
|
|
||||||
pub(crate) packet_data: Vec<u8>,
|
|
||||||
pub(crate) stage: Stage,
|
|
||||||
pub(crate) packet_type: i32,
|
|
||||||
pub(crate) packet_name: String,
|
|
||||||
pub(crate) created_at: OffsetDateTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Packet {
|
|
||||||
pub(crate) fn selected(&mut self, value: bool) {
|
|
||||||
self.selected = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_raw_packet(&self) -> Vec<u8> {
|
|
||||||
let mut dec = PacketDecoder::new();
|
|
||||||
dec.set_compression(self.compression_threshold);
|
|
||||||
dec.queue_slice(&self.packet_data);
|
|
||||||
|
|
||||||
match dec.try_next_packet() {
|
|
||||||
Ok(Some(data)) => data.into(),
|
|
||||||
Ok(None) => vec![],
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Error decoding packet: {e:#}");
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_packet_string(&self, formatted: bool) -> String {
|
|
||||||
let mut dec = PacketDecoder::new();
|
|
||||||
dec.set_compression(self.compression_threshold);
|
|
||||||
dec.queue_slice(&self.packet_data);
|
|
||||||
|
|
||||||
macro_rules! get {
|
|
||||||
($packet:ident) => {
|
|
||||||
match dec.try_next_packet() {
|
|
||||||
Ok(Some(frame)) => {
|
|
||||||
if let Ok(pkt) =
|
|
||||||
<$packet as valence::protocol::Packet>::decode_packet(&mut &frame[..])
|
|
||||||
{
|
|
||||||
if formatted {
|
|
||||||
format!("{pkt:#?}")
|
|
||||||
} else {
|
|
||||||
format!("{pkt:?}")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
stringify!($packet).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None) => stringify!($packet).into(),
|
|
||||||
Err(e) => format!("{e:#}"),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.stage {
|
|
||||||
Stage::HandshakeC2s => get!(HandshakeC2s),
|
|
||||||
Stage::QueryRequestC2s => get!(QueryRequestC2s),
|
|
||||||
Stage::QueryResponseS2c => get!(QueryResponseS2c),
|
|
||||||
Stage::QueryPingC2s => get!(QueryPingC2s),
|
|
||||||
Stage::QueryPongS2c => get!(QueryPongS2c),
|
|
||||||
Stage::LoginHelloC2s => get!(LoginHelloC2s),
|
|
||||||
Stage::S2cLoginPacket => get!(S2cLoginPacket),
|
|
||||||
Stage::LoginKeyC2s => get!(LoginKeyC2s),
|
|
||||||
Stage::LoginSuccessS2c => get!(LoginSuccessS2c),
|
|
||||||
Stage::C2sPlayPacket => get!(C2sPlayPacket),
|
|
||||||
Stage::S2cPlayPacket => get!(S2cPlayPacket),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Logger {
|
|
||||||
pub include_filter: Option<Regex>,
|
|
||||||
pub exclude_filter: Option<Regex>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum ContextMode {
|
|
||||||
Gui(egui::Context),
|
|
||||||
Cli(Logger),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Context {
|
|
||||||
pub mode: ContextMode,
|
|
||||||
pub last_packet: AtomicUsize,
|
|
||||||
pub selected_packet: RwLock<Option<usize>>,
|
|
||||||
pub(crate) packets: RwLock<Vec<Packet>>,
|
|
||||||
pub(crate) packet_count: RwLock<usize>,
|
|
||||||
pub(crate) has_encryption_enabled_error: AtomicBool,
|
|
||||||
pub filter: RwLock<String>,
|
|
||||||
pub visible_packets: RwLock<BTreeMap<MetaPacket, bool>>,
|
|
||||||
c2s_style: Style,
|
|
||||||
s2c_style: Style,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Context {
|
|
||||||
pub fn new(mode: ContextMode) -> Self {
|
|
||||||
Self {
|
|
||||||
mode,
|
|
||||||
last_packet: AtomicUsize::new(0),
|
|
||||||
selected_packet: RwLock::new(None),
|
|
||||||
packets: RwLock::new(Vec::new()),
|
|
||||||
|
|
||||||
filter: RwLock::new("".into()),
|
|
||||||
visible_packets: RwLock::new(BTreeMap::new()),
|
|
||||||
|
|
||||||
packet_count: RwLock::new(0),
|
|
||||||
|
|
||||||
has_encryption_enabled_error: AtomicBool::new(false),
|
|
||||||
|
|
||||||
c2s_style: Style::new().green(),
|
|
||||||
s2c_style: Style::new().purple(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear(&self) {
|
|
||||||
self.last_packet.store(0, Ordering::Relaxed);
|
|
||||||
*self.selected_packet.write().unwrap() = None;
|
|
||||||
self.packets.write().unwrap().clear();
|
|
||||||
if let ContextMode::Gui(ctx) = &self.mode {
|
|
||||||
ctx.request_repaint();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add(&self, mut packet: Packet) {
|
|
||||||
match &self.mode {
|
|
||||||
ContextMode::Gui(ctx) => {
|
|
||||||
packet.id = self.last_packet.fetch_add(1, Ordering::Relaxed);
|
|
||||||
self.packets.write().unwrap().push(packet);
|
|
||||||
ctx.request_repaint();
|
|
||||||
}
|
|
||||||
ContextMode::Cli(logger) => {
|
|
||||||
if let Some(include_filter) = &logger.include_filter {
|
|
||||||
if !include_filter.is_match(&packet.packet_name) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(exclude_filter) = &logger.exclude_filter {
|
|
||||||
if exclude_filter.is_match(&packet.packet_name) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let arrow = match &packet.direction {
|
|
||||||
PacketDirection::ClientToServer => "↑",
|
|
||||||
PacketDirection::ServerToClient => "↓",
|
|
||||||
};
|
|
||||||
|
|
||||||
if atty::is(atty::Stream::Stdout) {
|
|
||||||
let style = match &packet.direction {
|
|
||||||
PacketDirection::ClientToServer => self.c2s_style,
|
|
||||||
PacketDirection::ServerToClient => self.s2c_style,
|
|
||||||
};
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"[{}] ({}) {}",
|
|
||||||
systemtime_strftime(packet.created_at),
|
|
||||||
arrow.style(style),
|
|
||||||
packet.get_packet_string(false).style(style)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
println!(
|
|
||||||
"[{}] ({}) {}",
|
|
||||||
systemtime_strftime(packet.created_at),
|
|
||||||
arrow,
|
|
||||||
packet.get_packet_string(false)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_selected_packets(&self, packets: BTreeMap<MetaPacket, bool>) {
|
|
||||||
*self.visible_packets.write().unwrap() = packets;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_packet_hidden(&self, index: usize) -> bool {
|
|
||||||
let packets = self.packets.read().unwrap();
|
|
||||||
let packet = packets.get(index).expect("Packet not found");
|
|
||||||
|
|
||||||
let visible_packets = self.visible_packets.read().unwrap();
|
|
||||||
|
|
||||||
let meta_packet: MetaPacket = (*packet).clone().into();
|
|
||||||
|
|
||||||
if let Some(visible) = visible_packets.get(&meta_packet) {
|
|
||||||
if !visible {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let filter = self.filter.read().unwrap();
|
|
||||||
let filter = filter.as_str();
|
|
||||||
if !filter.is_empty()
|
|
||||||
&& packet
|
|
||||||
.packet_name
|
|
||||||
.to_lowercase()
|
|
||||||
.contains(&filter.to_lowercase())
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select_previous_packet(&self) {
|
|
||||||
let mut selected_packet = self.selected_packet.write().unwrap();
|
|
||||||
if let Some(idx) = *selected_packet {
|
|
||||||
if idx > 0 {
|
|
||||||
let mut new_index = idx - 1;
|
|
||||||
while self.is_packet_hidden(new_index) {
|
|
||||||
if new_index == 0 {
|
|
||||||
new_index = idx;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
new_index -= 1;
|
|
||||||
}
|
|
||||||
*selected_packet = Some(new_index);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let packets = self.packets.read().unwrap();
|
|
||||||
if !packets.is_empty() {
|
|
||||||
*selected_packet = Some(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select_next_packet(&self) {
|
|
||||||
let mut selected_packet = self.selected_packet.write().unwrap();
|
|
||||||
if let Some(idx) = *selected_packet {
|
|
||||||
if idx < self.packets.read().unwrap().len() - 1 {
|
|
||||||
let mut new_index = idx + 1;
|
|
||||||
while self.is_packet_hidden(new_index) {
|
|
||||||
if new_index == self.packets.read().unwrap().len() - 1 {
|
|
||||||
new_index = idx;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
new_index += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
*selected_packet = Some(new_index);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let packets = self.packets.read().unwrap();
|
|
||||||
if !packets.is_empty() {
|
|
||||||
*selected_packet = Some(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_selected_packet(&self, idx: usize) {
|
|
||||||
*self.selected_packet.write().unwrap() = Some(idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_filter(&self, filter: String) {
|
|
||||||
*self.filter.write().expect("Posisoned RwLock") = filter;
|
|
||||||
*self.selected_packet.write().unwrap() = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save(&self, path: PathBuf) -> Result<(), std::io::Error> {
|
|
||||||
let packets = self
|
|
||||||
.packets
|
|
||||||
.read()
|
|
||||||
.unwrap()
|
|
||||||
.iter()
|
|
||||||
.map(|packet| {
|
|
||||||
format!(
|
|
||||||
"[{}] {}",
|
|
||||||
systemtime_strftime(packet.created_at),
|
|
||||||
packet.get_packet_string(false)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
std::fs::write(path, packets)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
use std::io::Read;
|
|
||||||
|
|
||||||
use egui::Color32;
|
|
||||||
|
|
||||||
pub fn hex_view_ui(ui: &mut egui::Ui, mut file: &[u8]) {
|
|
||||||
let mut buf = [0u8; 16];
|
|
||||||
let mut count = 0;
|
|
||||||
|
|
||||||
egui::Grid::new("hex_grid")
|
|
||||||
.spacing([4.0, 1.5])
|
|
||||||
.striped(true)
|
|
||||||
.min_col_width(0.0)
|
|
||||||
.show(ui, |ui| {
|
|
||||||
ui.label(" ");
|
|
||||||
for i in 0..16 {
|
|
||||||
ui.label(format!("{:02X}", i));
|
|
||||||
}
|
|
||||||
ui.end_row();
|
|
||||||
loop {
|
|
||||||
let bytes_read = file.read(&mut buf).unwrap();
|
|
||||||
if bytes_read == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.label(format!("{:08X}", count));
|
|
||||||
for b in buf.iter().take(bytes_read) {
|
|
||||||
ui.colored_label(Color32::from_rgb(255, 255, 255), format!("{:02X}", b));
|
|
||||||
}
|
|
||||||
for _ in 0..16 - bytes_read {
|
|
||||||
ui.label(" ");
|
|
||||||
}
|
|
||||||
ui.label(" ");
|
|
||||||
for b in buf.iter().take(bytes_read) {
|
|
||||||
if *b >= 0x20 && *b <= 0x7e {
|
|
||||||
ui.label(format!("{}", *b as char));
|
|
||||||
} else {
|
|
||||||
ui.label(".");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.end_row();
|
|
||||||
count += bytes_read;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
188
tools/packet_inspector/src/lib.rs
Normal file
188
tools/packet_inspector/src/lib.rs
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
mod packet_io;
|
||||||
|
mod packet_registry;
|
||||||
|
|
||||||
|
use std::{net::SocketAddr, sync::OnceLock};
|
||||||
|
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
use valence::network::packet::{
|
||||||
|
HandshakeC2s, HandshakeNextState, LoginCompressionS2c, LoginSuccessS2c,
|
||||||
|
};
|
||||||
|
use valence::protocol::{decode::PacketFrame, Decode, Packet as ValencePacket};
|
||||||
|
|
||||||
|
use crate::{packet_io::PacketIo, packet_registry::PacketRegistry};
|
||||||
|
|
||||||
|
pub use packet_registry::Packet;
|
||||||
|
|
||||||
|
pub use crate::packet_registry::PacketSide;
|
||||||
|
pub use crate::packet_registry::PacketState;
|
||||||
|
|
||||||
|
static PACKET_REGISTRY: OnceLock<Arc<RwLock<PacketRegistry>>> = OnceLock::new();
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/packets.rs"));
|
||||||
|
|
||||||
|
pub struct Proxy {
|
||||||
|
listener_addr: SocketAddr,
|
||||||
|
server_addr: SocketAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Proxy {
|
||||||
|
pub fn new(listener_addr: SocketAddr, server_addr: SocketAddr) -> Self {
|
||||||
|
PACKET_REGISTRY.get_or_init(|| {
|
||||||
|
let registry = PacketRegistry::new();
|
||||||
|
registry.register_all(&STD_PACKETS);
|
||||||
|
Arc::new(RwLock::new(registry))
|
||||||
|
});
|
||||||
|
|
||||||
|
Proxy {
|
||||||
|
listener_addr,
|
||||||
|
server_addr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn subscribe(&self) -> flume::Receiver<Packet> {
|
||||||
|
PACKET_REGISTRY.get().unwrap().read().await.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(&self) -> anyhow::Result<()> {
|
||||||
|
let listener = tokio::net::TcpListener::bind(self.listener_addr).await?;
|
||||||
|
|
||||||
|
while let Ok((client, _addr)) = listener.accept().await {
|
||||||
|
let server_addr = self.server_addr;
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let server = TcpStream::connect(server_addr).await?;
|
||||||
|
|
||||||
|
if let Err(e) = Self::process(client, server).await {
|
||||||
|
if !e.to_string().contains("unexpected end of file") {
|
||||||
|
// bit meh to do it like this but it works
|
||||||
|
tracing::error!("Error: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<(), anyhow::Error>(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process(client: TcpStream, server: TcpStream) -> anyhow::Result<()> {
|
||||||
|
let client = PacketIo::new(client);
|
||||||
|
let server = PacketIo::new(server);
|
||||||
|
|
||||||
|
let (mut client_reader, mut client_writer) = client.split();
|
||||||
|
let (mut server_reader, mut server_writer) = server.split();
|
||||||
|
|
||||||
|
let current_state_inner = Arc::new(RwLock::new(PacketState::Handshaking));
|
||||||
|
let threshold_inner: Arc<RwLock<Option<u32>>> = Arc::new(RwLock::new(None));
|
||||||
|
|
||||||
|
let current_state = current_state_inner.clone();
|
||||||
|
let threshold = threshold_inner.clone();
|
||||||
|
let registry = PACKET_REGISTRY.get().unwrap().clone();
|
||||||
|
let c2s = tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let threashold = *threshold.read().await;
|
||||||
|
client_reader.set_compression(threashold);
|
||||||
|
server_writer.set_compression(threashold);
|
||||||
|
// client to server handling
|
||||||
|
let packet = client_reader.recv_packet_raw().await?;
|
||||||
|
|
||||||
|
let state = {
|
||||||
|
let state = current_state.read().await;
|
||||||
|
*state
|
||||||
|
};
|
||||||
|
|
||||||
|
let registry = registry.write().await;
|
||||||
|
registry
|
||||||
|
.process(
|
||||||
|
crate::packet_registry::PacketSide::Serverbound,
|
||||||
|
state,
|
||||||
|
threashold,
|
||||||
|
&packet,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if state == PacketState::Handshaking {
|
||||||
|
if let Some(handshake) = extrapolate_packet::<HandshakeC2s>(&packet) {
|
||||||
|
*current_state.write().await = match handshake.next_state {
|
||||||
|
HandshakeNextState::Status => PacketState::Status,
|
||||||
|
HandshakeNextState::Login => PacketState::Login,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server_writer.send_packet_raw(&packet).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unreachable_code)]
|
||||||
|
Ok::<(), anyhow::Error>(())
|
||||||
|
});
|
||||||
|
|
||||||
|
let current_state = current_state_inner.clone();
|
||||||
|
let threshold = threshold_inner.clone();
|
||||||
|
let registry = PACKET_REGISTRY.get().unwrap().clone();
|
||||||
|
let s2c = tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let threashold_value = *threshold.read().await;
|
||||||
|
server_reader.set_compression(threashold_value);
|
||||||
|
client_writer.set_compression(threashold_value);
|
||||||
|
// server to client handling
|
||||||
|
let packet = server_reader.recv_packet_raw().await?;
|
||||||
|
|
||||||
|
let state = {
|
||||||
|
let state = current_state.read().await;
|
||||||
|
*state
|
||||||
|
};
|
||||||
|
|
||||||
|
if state == PacketState::Login {
|
||||||
|
if let Some(compression) = extrapolate_packet::<LoginCompressionS2c>(&packet) {
|
||||||
|
if compression.threshold.0 >= 0 {
|
||||||
|
*threshold.write().await = Some(compression.threshold.0 as u32);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if extrapolate_packet::<LoginSuccessS2c>(&packet).is_some() {
|
||||||
|
*current_state.write().await = PacketState::Play;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
registry
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.process(
|
||||||
|
crate::packet_registry::PacketSide::Clientbound,
|
||||||
|
state,
|
||||||
|
threashold_value,
|
||||||
|
&packet,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
client_writer.send_packet_raw(&packet).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unreachable_code)]
|
||||||
|
Ok::<(), anyhow::Error>(())
|
||||||
|
});
|
||||||
|
|
||||||
|
// wait for either to finish
|
||||||
|
tokio::select! {
|
||||||
|
res = c2s => res?,
|
||||||
|
res = s2c => res?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extrapolate_packet<'a, P>(packet: &'a PacketFrame) -> Option<P>
|
||||||
|
where
|
||||||
|
P: ValencePacket + Decode<'a> + Clone,
|
||||||
|
{
|
||||||
|
if packet.id != P::ID {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut r = &packet.body[..];
|
||||||
|
let packet = P::decode(&mut r).ok()?;
|
||||||
|
Some(packet)
|
||||||
|
}
|
|
@ -1,957 +1,18 @@
|
||||||
#![doc = include_str!("../README.md")]
|
#![cfg_attr(
|
||||||
#![deny(
|
all(not(debug_assertions), feature = "gui"),
|
||||||
rustdoc::broken_intra_doc_links,
|
windows_subsystem = "windows"
|
||||||
rustdoc::private_intra_doc_links,
|
|
||||||
rustdoc::missing_crate_level_docs,
|
|
||||||
rustdoc::invalid_codeblock_attributes,
|
|
||||||
rustdoc::invalid_rust_codeblocks,
|
|
||||||
rustdoc::bare_urls,
|
|
||||||
rustdoc::invalid_html_tags
|
|
||||||
)]
|
|
||||||
#![warn(
|
|
||||||
trivial_casts,
|
|
||||||
trivial_numeric_casts,
|
|
||||||
unused_lifetimes,
|
|
||||||
unused_import_braces,
|
|
||||||
clippy::dbg_macro
|
|
||||||
)]
|
)]
|
||||||
|
|
||||||
mod config;
|
#[cfg(any(
|
||||||
mod context;
|
all(not(feature = "gui"), not(feature = "cli")),
|
||||||
mod hex_viewer;
|
all(feature = "gui", feature = "cli")
|
||||||
pub mod packet_groups;
|
))]
|
||||||
mod packet_widget;
|
fn main() {
|
||||||
mod state;
|
panic!("Invalid features; select either \"cli\" or \"gui\"");
|
||||||
mod syntax_highlighting;
|
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::sync::atomic::Ordering;
|
|
||||||
use std::sync::{Arc, RwLock};
|
|
||||||
|
|
||||||
use anyhow::bail;
|
|
||||||
use bytes::BytesMut;
|
|
||||||
use clap::Parser;
|
|
||||||
use config::ApplicationConfig;
|
|
||||||
use context::{Context, Packet};
|
|
||||||
use egui::{Align2, RichText};
|
|
||||||
use hex_viewer::hex_view_ui;
|
|
||||||
use regex::Regex;
|
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
|
||||||
use syntax_highlighting::code_view_ui;
|
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
||||||
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
|
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
|
||||||
use tokio::sync::Semaphore;
|
|
||||||
use tokio::task::JoinHandle;
|
|
||||||
use tracing_subscriber::filter::LevelFilter;
|
|
||||||
use valence::network::packet::{
|
|
||||||
HandshakeC2s, HandshakeNextState, LoginHelloC2s, LoginKeyC2s, LoginSuccessS2c, QueryPingC2s,
|
|
||||||
QueryPongS2c, QueryRequestC2s, QueryResponseS2c,
|
|
||||||
};
|
|
||||||
use valence::protocol::decode::PacketDecoder;
|
|
||||||
use valence::protocol::encode::PacketEncoder;
|
|
||||||
|
|
||||||
use crate::context::{ContextMode, Stage};
|
|
||||||
use crate::packet_groups::{C2sPlayPacket, S2cLoginPacket, S2cPlayPacket};
|
|
||||||
use crate::packet_widget::PacketDirection;
|
|
||||||
use crate::state::State;
|
|
||||||
|
|
||||||
#[derive(Parser, Clone, Debug)]
|
|
||||||
#[clap(author, version, about)]
|
|
||||||
struct Cli {
|
|
||||||
/// The socket address to listen for connections on. This is the address
|
|
||||||
/// clients should connect to.
|
|
||||||
#[arg(required_if_eq("nogui", "true"))]
|
|
||||||
client_addr: Option<SocketAddr>,
|
|
||||||
|
|
||||||
/// The socket address the proxy will connect to. This is the address of the
|
|
||||||
/// server.
|
|
||||||
#[arg(required_if_eq("nogui", "true"))]
|
|
||||||
server_addr: Option<SocketAddr>,
|
|
||||||
|
|
||||||
/// The maximum number of connections allowed to the proxy. By default,
|
|
||||||
/// there is no limit.
|
|
||||||
#[clap(short, long)]
|
|
||||||
max_connections: Option<usize>,
|
|
||||||
|
|
||||||
/// Disable the GUI. Logging to stdout.
|
|
||||||
#[clap(long)]
|
|
||||||
nogui: bool,
|
|
||||||
|
|
||||||
/// Only show packets that match the filter.
|
|
||||||
#[clap(short, long)]
|
|
||||||
include_filter: Option<Regex>,
|
|
||||||
|
|
||||||
/// Hide packets that match the filter. Note: Only in effect if nogui is
|
|
||||||
/// set.
|
|
||||||
#[clap(short, long)]
|
|
||||||
exclude_filter: Option<Regex>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[cfg(all(not(feature = "cli"), feature = "gui"))]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
include!("./main_gui.rs");
|
||||||
tracing_subscriber::fmt()
|
|
||||||
.with_max_level(LevelFilter::DEBUG)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
let cli = Arc::new(Cli::parse());
|
#[cfg(all(not(feature = "gui"), feature = "cli"))]
|
||||||
|
include!("./main_cli.rs");
|
||||||
match cli.nogui {
|
|
||||||
true => start_cli(cli).await?,
|
|
||||||
false => start_gui(cli)?,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn start_cli(cli: Arc<Cli>) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let context = Arc::new(Context::new(ContextMode::Cli(context::Logger {
|
|
||||||
include_filter: cli.include_filter.clone(),
|
|
||||||
exclude_filter: cli.exclude_filter.clone(),
|
|
||||||
})));
|
|
||||||
|
|
||||||
let sema = Arc::new(Semaphore::new(cli.max_connections.unwrap_or(100_000)));
|
|
||||||
|
|
||||||
let client_addr = match cli.client_addr {
|
|
||||||
Some(addr) => addr,
|
|
||||||
None => return Err("Missing Client Address".into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let server_addr = match cli.server_addr {
|
|
||||||
Some(addr) => addr,
|
|
||||||
None => return Err("Missing Server Address".into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
eprintln!("Waiting for connections on {}", client_addr);
|
|
||||||
let listen = TcpListener::bind(client_addr).await?;
|
|
||||||
|
|
||||||
while let Ok(permit) = sema.clone().acquire_owned().await {
|
|
||||||
let (client, remote_client_addr) = listen.accept().await?;
|
|
||||||
eprintln!("Accepted connection to {remote_client_addr}");
|
|
||||||
|
|
||||||
if let Err(e) = client.set_nodelay(true) {
|
|
||||||
eprintln!("Failed to set TCP_NODELAY: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
let context = context.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(e) = handle_connection(client, server_addr, context).await {
|
|
||||||
eprintln!("Connection to {remote_client_addr} ended with: {e:#}");
|
|
||||||
} else {
|
|
||||||
eprintln!("Connection to {remote_client_addr} ended.");
|
|
||||||
}
|
|
||||||
drop(permit);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_gui(cli: Arc<Cli>) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let native_options = eframe::NativeOptions {
|
|
||||||
initial_window_size: Some(egui::Vec2::new(800.0, 600.0)),
|
|
||||||
decorated: true,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let server_addr = cli.server_addr;
|
|
||||||
let client_addr = cli.client_addr;
|
|
||||||
let max_connections = cli.max_connections.unwrap_or(100_000);
|
|
||||||
|
|
||||||
let filter = cli
|
|
||||||
.include_filter
|
|
||||||
.clone()
|
|
||||||
.map(|f| f.to_string())
|
|
||||||
.unwrap_or("".to_string());
|
|
||||||
|
|
||||||
eframe::run_native(
|
|
||||||
"Valence Packet Inspector",
|
|
||||||
native_options,
|
|
||||||
Box::new(move |cc| {
|
|
||||||
let gui_app = GuiApp::new(cc, filter);
|
|
||||||
|
|
||||||
if let Some(server_addr) = server_addr {
|
|
||||||
if let Some(client_addr) = client_addr {
|
|
||||||
gui_app.start_listening(client_addr, server_addr, max_connections);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Box::new(gui_app)
|
|
||||||
}),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_connection(
|
|
||||||
client: TcpStream,
|
|
||||||
server_addr: SocketAddr,
|
|
||||||
context: Arc<Context>,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
eprintln!("Connecting to {}", server_addr);
|
|
||||||
|
|
||||||
let server = TcpStream::connect(server_addr).await?;
|
|
||||||
|
|
||||||
if let Err(e) = server.set_nodelay(true) {
|
|
||||||
eprintln!("Failed to set TCP_NODELAY: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
let (client_read, client_write) = client.into_split();
|
|
||||||
let (server_read, server_write) = server.into_split();
|
|
||||||
|
|
||||||
let mut s2c = State {
|
|
||||||
enc: PacketEncoder::new(),
|
|
||||||
dec: PacketDecoder::new(),
|
|
||||||
read: server_read,
|
|
||||||
write: client_write,
|
|
||||||
direction: PacketDirection::ServerToClient,
|
|
||||||
context: context.clone(),
|
|
||||||
frame: BytesMut::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut c2s = State {
|
|
||||||
enc: PacketEncoder::new(),
|
|
||||||
dec: PacketDecoder::new(),
|
|
||||||
read: client_read,
|
|
||||||
write: server_write,
|
|
||||||
direction: PacketDirection::ClientToServer,
|
|
||||||
context: context.clone(),
|
|
||||||
frame: BytesMut::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let handshake: HandshakeC2s = c2s.rw_packet(Stage::HandshakeC2s).await?;
|
|
||||||
|
|
||||||
match handshake.next_state {
|
|
||||||
HandshakeNextState::Status => {
|
|
||||||
c2s.rw_packet::<QueryRequestC2s>(Stage::QueryRequestC2s)
|
|
||||||
.await?;
|
|
||||||
s2c.rw_packet::<QueryResponseS2c>(Stage::QueryResponseS2c)
|
|
||||||
.await?;
|
|
||||||
c2s.rw_packet::<QueryPingC2s>(Stage::QueryPingC2s).await?;
|
|
||||||
s2c.rw_packet::<QueryPongS2c>(Stage::QueryPongS2c).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
HandshakeNextState::Login => {
|
|
||||||
c2s.rw_packet::<LoginHelloC2s>(Stage::LoginHelloC2s).await?;
|
|
||||||
|
|
||||||
match s2c
|
|
||||||
.rw_packet::<S2cLoginPacket>(Stage::S2cLoginPacket)
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
S2cLoginPacket::LoginHelloS2c(_) => {
|
|
||||||
c2s.rw_packet::<LoginKeyC2s>(Stage::LoginKeyC2s).await?;
|
|
||||||
|
|
||||||
eprintln!(
|
|
||||||
"Encryption is enabled! Packet contents are inaccessible to the proxy. \
|
|
||||||
Disable online mode to fix this."
|
|
||||||
);
|
|
||||||
|
|
||||||
context
|
|
||||||
.has_encryption_enabled_error
|
|
||||||
.store(true, Ordering::Relaxed);
|
|
||||||
|
|
||||||
return tokio::select! {
|
|
||||||
c2s_res = passthrough(c2s.read, c2s.write) => c2s_res,
|
|
||||||
s2c_res = passthrough(s2c.read, s2c.write) => s2c_res,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
S2cLoginPacket::LoginCompressionS2c(pkt) => {
|
|
||||||
let threshold = pkt.threshold.0 as u32;
|
|
||||||
|
|
||||||
s2c.enc.set_compression(Some(threshold));
|
|
||||||
s2c.dec.set_compression(Some(threshold));
|
|
||||||
c2s.enc.set_compression(Some(threshold));
|
|
||||||
c2s.dec.set_compression(Some(threshold));
|
|
||||||
|
|
||||||
s2c.rw_packet::<LoginSuccessS2c>(Stage::LoginSuccessS2c)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
S2cLoginPacket::LoginSuccessS2c(_) => {}
|
|
||||||
S2cLoginPacket::LoginDisconnectS2c(_) => return Ok(()),
|
|
||||||
S2cLoginPacket::LoginQueryRequestS2c(_) => {
|
|
||||||
bail!("Got login plugin request. Don't know how to proceed.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let c2s_fut: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
c2s.rw_packet::<C2sPlayPacket>(Stage::C2sPlayPacket).await?;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let s2c_fut: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
s2c.rw_packet::<S2cPlayPacket>(Stage::S2cPlayPacket).await?;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tokio::select! {
|
|
||||||
c2s = c2s_fut => Ok(c2s??),
|
|
||||||
s2c = s2c_fut => Ok(s2c??),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn passthrough(mut read: OwnedReadHalf, mut write: OwnedWriteHalf) -> anyhow::Result<()> {
|
|
||||||
let mut buf = Box::new([0u8; 8192]);
|
|
||||||
loop {
|
|
||||||
let bytes_read = read.read(buf.as_mut_slice()).await?;
|
|
||||||
let bytes = &mut buf[..bytes_read];
|
|
||||||
|
|
||||||
if bytes.is_empty() {
|
|
||||||
break Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
write.write_all(bytes).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct MetaPacket {
|
|
||||||
id: i32,
|
|
||||||
stage: Stage,
|
|
||||||
direction: PacketDirection,
|
|
||||||
name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
// manually implement Serialize and Deserialize that use the ToString and
|
|
||||||
// FromStr implementaions for keys
|
|
||||||
impl Serialize for MetaPacket {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
serializer.serialize_str(&self.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for MetaPacket {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let s = String::deserialize(deserializer)?;
|
|
||||||
MetaPacket::from_str(&s).map_err(serde::de::Error::custom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<(Stage, i32, PacketDirection, String)> for MetaPacket {
|
|
||||||
fn from((stage, id, direction, name): (Stage, i32, PacketDirection, String)) -> Self {
|
|
||||||
Self {
|
|
||||||
stage,
|
|
||||||
id,
|
|
||||||
direction,
|
|
||||||
name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Packet> for MetaPacket {
|
|
||||||
fn from(packet: Packet) -> Self {
|
|
||||||
Self {
|
|
||||||
stage: packet.stage,
|
|
||||||
id: packet.packet_type,
|
|
||||||
direction: packet.direction,
|
|
||||||
name: packet.packet_name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// to string and from string to be used in toml
|
|
||||||
impl ToString for MetaPacket {
|
|
||||||
fn to_string(&self) -> String {
|
|
||||||
let stage: usize = self.stage.clone().into();
|
|
||||||
|
|
||||||
format!(
|
|
||||||
"{}:{}:{}:{}",
|
|
||||||
stage,
|
|
||||||
self.id,
|
|
||||||
match self.direction {
|
|
||||||
PacketDirection::ClientToServer => 0,
|
|
||||||
PacketDirection::ServerToClient => 1,
|
|
||||||
},
|
|
||||||
self.name
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for MetaPacket {
|
|
||||||
type Err = anyhow::Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
let mut split = s.split(':');
|
|
||||||
let stage = match split.next().unwrap().parse::<usize>() {
|
|
||||||
Ok(stage) => Stage::try_from(stage)?,
|
|
||||||
Err(_) => bail!("invalid stage"),
|
|
||||||
};
|
|
||||||
let id = split.next().unwrap().parse::<i32>()?;
|
|
||||||
let direction = match split.next().unwrap().parse::<i32>()? {
|
|
||||||
0 => PacketDirection::ClientToServer,
|
|
||||||
1 => PacketDirection::ServerToClient,
|
|
||||||
_ => bail!("invalid direction"),
|
|
||||||
};
|
|
||||||
let name = split.next().unwrap().to_string();
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
stage,
|
|
||||||
id,
|
|
||||||
direction,
|
|
||||||
name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ord for MetaPacket {
|
|
||||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
||||||
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
struct OrdMetaPacket {
|
|
||||||
stage: Stage,
|
|
||||||
id: i32,
|
|
||||||
direction: PacketDirection,
|
|
||||||
}
|
|
||||||
|
|
||||||
let left = OrdMetaPacket {
|
|
||||||
stage: self.stage.clone(),
|
|
||||||
id: self.id,
|
|
||||||
direction: self.direction.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let right = OrdMetaPacket {
|
|
||||||
stage: other.stage.clone(),
|
|
||||||
id: other.id,
|
|
||||||
direction: other.direction.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
left.cmp(&right)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialOrd for MetaPacket {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> std::option::Option<std::cmp::Ordering> {
|
|
||||||
Some(self.cmp(other))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for MetaPacket {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.stage == other.stage && self.id == other.id && self.direction == other.direction
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for MetaPacket {}
|
|
||||||
|
|
||||||
struct GuiApp {
|
|
||||||
config: ApplicationConfig,
|
|
||||||
temp_server_addr: String,
|
|
||||||
temp_client_addr: String,
|
|
||||||
temp_max_connections: String,
|
|
||||||
|
|
||||||
server_addr_error: bool,
|
|
||||||
client_addr_error: bool,
|
|
||||||
max_connections_error: bool,
|
|
||||||
|
|
||||||
context: Arc<Context>,
|
|
||||||
filter: String,
|
|
||||||
|
|
||||||
selected_packets: BTreeMap<MetaPacket, bool>,
|
|
||||||
packet_filter: String,
|
|
||||||
|
|
||||||
buffer: String,
|
|
||||||
is_listening: RwLock<bool>,
|
|
||||||
window_open: bool,
|
|
||||||
encryption_error_dialog_open: bool,
|
|
||||||
|
|
||||||
config_load_error: Option<String>,
|
|
||||||
config_load_error_window_open: bool,
|
|
||||||
|
|
||||||
raw_packet: Vec<u8>,
|
|
||||||
view_hex: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GuiApp {
|
|
||||||
fn new(cc: &eframe::CreationContext<'_>, filter: String) -> Self {
|
|
||||||
let ctx = cc.egui_ctx.clone();
|
|
||||||
|
|
||||||
let context = Context::new(ContextMode::Gui(ctx));
|
|
||||||
|
|
||||||
let mut config_load_error: Option<String> = None;
|
|
||||||
|
|
||||||
let mut config = match ApplicationConfig::load() {
|
|
||||||
Ok(config) => config,
|
|
||||||
Err(e) => {
|
|
||||||
config_load_error = Some(format!("Failed to load config:\n{}", e));
|
|
||||||
ApplicationConfig::default()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut filter = filter;
|
|
||||||
|
|
||||||
if filter.is_empty() {
|
|
||||||
if let Some(c_filter) = config.filter() {
|
|
||||||
filter = c_filter.to_string();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
config.set_filter(Some(filter.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut f = context.filter.write().expect("Poisoned filter");
|
|
||||||
*f = filter.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
let context = Arc::new(context);
|
|
||||||
|
|
||||||
let temp_server_addr = config.server_addr().to_string();
|
|
||||||
let temp_client_addr = config.client_addr().to_string();
|
|
||||||
let temp_max_connections = match config.max_connections() {
|
|
||||||
Some(max_connections) => max_connections.to_string(),
|
|
||||||
None => String::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let selected_packets = match config.selected_packets().clone() {
|
|
||||||
Some(selected_packets) => selected_packets,
|
|
||||||
None => BTreeMap::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
context.set_selected_packets(selected_packets.clone());
|
|
||||||
|
|
||||||
Self {
|
|
||||||
config,
|
|
||||||
context,
|
|
||||||
filter,
|
|
||||||
|
|
||||||
selected_packets,
|
|
||||||
packet_filter: String::new(),
|
|
||||||
|
|
||||||
buffer: String::new(),
|
|
||||||
is_listening: RwLock::new(false),
|
|
||||||
window_open: false,
|
|
||||||
encryption_error_dialog_open: false,
|
|
||||||
|
|
||||||
temp_server_addr,
|
|
||||||
temp_client_addr,
|
|
||||||
temp_max_connections,
|
|
||||||
|
|
||||||
server_addr_error: false,
|
|
||||||
client_addr_error: false,
|
|
||||||
max_connections_error: false,
|
|
||||||
|
|
||||||
config_load_error,
|
|
||||||
config_load_error_window_open: false,
|
|
||||||
|
|
||||||
raw_packet: Vec::new(),
|
|
||||||
view_hex: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_listening(
|
|
||||||
&self,
|
|
||||||
client_addr: SocketAddr,
|
|
||||||
server_addr: SocketAddr,
|
|
||||||
max_connections: usize,
|
|
||||||
) {
|
|
||||||
let t_context = self.context.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let sema = Arc::new(Semaphore::new(max_connections));
|
|
||||||
|
|
||||||
let listen = TcpListener::bind(client_addr).await?;
|
|
||||||
eprintln!("Waiting for connections on {}", client_addr);
|
|
||||||
|
|
||||||
while let Ok(permit) = sema.clone().acquire_owned().await {
|
|
||||||
let (client, remote_client_addr) = listen.accept().await?;
|
|
||||||
eprintln!("Accepted connection to {remote_client_addr}");
|
|
||||||
|
|
||||||
if let Err(e) = client.set_nodelay(true) {
|
|
||||||
eprintln!("Failed to set TCP_NODELAY: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
let t2_context = t_context.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(e) = handle_connection(client, server_addr, t2_context).await {
|
|
||||||
eprintln!("Connection to {remote_client_addr} ended with: {e:#}");
|
|
||||||
} else {
|
|
||||||
eprintln!("Connection to {remote_client_addr} ended.");
|
|
||||||
}
|
|
||||||
drop(permit);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok::<(), anyhow::Error>(())
|
|
||||||
});
|
|
||||||
*self.is_listening.write().expect("Poisoned is_listening") = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nested_menus(&mut self, ui: &mut egui::Ui) {
|
|
||||||
let mut changed = false;
|
|
||||||
self.selected_packets
|
|
||||||
.iter_mut()
|
|
||||||
.filter(|(m_packet, _)| {
|
|
||||||
self.packet_filter.is_empty()
|
|
||||||
|| m_packet
|
|
||||||
.name
|
|
||||||
.to_lowercase()
|
|
||||||
.contains(&self.packet_filter.to_lowercase())
|
|
||||||
})
|
|
||||||
.for_each(|(m_packet, selected)| {
|
|
||||||
// todo: format, add arrows, etc
|
|
||||||
if ui.checkbox(selected, m_packet.name.clone()).changed() {
|
|
||||||
changed = true;
|
|
||||||
ui.ctx().request_repaint();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if changed {
|
|
||||||
self.config
|
|
||||||
.set_selected_packets(self.selected_packets.clone());
|
|
||||||
self.context
|
|
||||||
.set_selected_packets(self.selected_packets.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl eframe::App for GuiApp {
|
|
||||||
fn on_close_event(&mut self) -> bool {
|
|
||||||
match self.config.save() {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Failed to save config: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
|
||||||
if !*self.is_listening.read().expect("Poisoned is_listening") {
|
|
||||||
self.window_open = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self
|
|
||||||
.context
|
|
||||||
.has_encryption_enabled_error
|
|
||||||
.load(Ordering::Relaxed)
|
|
||||||
{
|
|
||||||
self.encryption_error_dialog_open = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.encryption_error_dialog_open {
|
|
||||||
egui::Window::new("Encryption Error")
|
|
||||||
.anchor(Align2::CENTER_CENTER, [0.0, 0.0])
|
|
||||||
.open(&mut self.encryption_error_dialog_open)
|
|
||||||
.movable(false)
|
|
||||||
.collapsible(false)
|
|
||||||
.resizable(false)
|
|
||||||
.show(ctx, |ui| {
|
|
||||||
ui.label(
|
|
||||||
"Encryption is enabled! Packet contents are inaccessible to the proxy. \
|
|
||||||
Disable online mode to fix this.",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// it was true, now it's false, the user acknowledged the error, set it to false
|
|
||||||
if !self.encryption_error_dialog_open {
|
|
||||||
self.context
|
|
||||||
.has_encryption_enabled_error
|
|
||||||
.store(false, Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.config_load_error.is_some() {
|
|
||||||
self.config_load_error_window_open = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.config_load_error_window_open {
|
|
||||||
if let Some(err) = &self.config_load_error {
|
|
||||||
egui::Window::new("Config Error")
|
|
||||||
.anchor(Align2::CENTER_CENTER, [0.0, 0.0])
|
|
||||||
.open(&mut self.config_load_error_window_open)
|
|
||||||
.movable(false)
|
|
||||||
.collapsible(false)
|
|
||||||
.resizable(false)
|
|
||||||
.show(ctx, |ui| {
|
|
||||||
ui.label(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.config_load_error_window_open {
|
|
||||||
self.config_load_error = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.window_open {
|
|
||||||
egui::Window::new("Setup")
|
|
||||||
.anchor(Align2::CENTER_CENTER, [0.0, 0.0])
|
|
||||||
.movable(false)
|
|
||||||
.collapsible(false)
|
|
||||||
.resizable(false)
|
|
||||||
.show(ctx, |ui| {
|
|
||||||
egui::Grid::new("setup_grid")
|
|
||||||
.num_columns(2)
|
|
||||||
.spacing([40.0, 4.0])
|
|
||||||
.striped(true)
|
|
||||||
.show(ui, |ui| {
|
|
||||||
ui.label(RichText::new("Server address:").color(
|
|
||||||
match self.server_addr_error {
|
|
||||||
true => egui::Color32::RED,
|
|
||||||
false => egui::Color32::WHITE,
|
|
||||||
},
|
|
||||||
));
|
|
||||||
if ui
|
|
||||||
.text_edit_singleline(&mut self.temp_server_addr)
|
|
||||||
.on_hover_text(
|
|
||||||
"The socket address the proxy will connect to. This is the \
|
|
||||||
address of the server.",
|
|
||||||
)
|
|
||||||
.changed()
|
|
||||||
{
|
|
||||||
self.server_addr_error = false;
|
|
||||||
};
|
|
||||||
ui.end_row();
|
|
||||||
ui.label(RichText::new("Client address:").color(
|
|
||||||
match self.client_addr_error {
|
|
||||||
true => egui::Color32::RED,
|
|
||||||
false => egui::Color32::WHITE,
|
|
||||||
},
|
|
||||||
));
|
|
||||||
ui.text_edit_singleline(&mut self.temp_client_addr)
|
|
||||||
.on_hover_text(
|
|
||||||
"The socket address to listen for connections on. This is the \
|
|
||||||
address clients should connect to.",
|
|
||||||
);
|
|
||||||
ui.end_row();
|
|
||||||
ui.label(RichText::new("Max Connections:").color(
|
|
||||||
match self.max_connections_error {
|
|
||||||
true => egui::Color32::RED,
|
|
||||||
false => egui::Color32::WHITE,
|
|
||||||
},
|
|
||||||
));
|
|
||||||
ui.text_edit_singleline(&mut self.temp_max_connections)
|
|
||||||
.on_hover_text(
|
|
||||||
"The maximum number of connections allowed to the proxy. By \
|
|
||||||
default, there is no limit.",
|
|
||||||
);
|
|
||||||
ui.end_row();
|
|
||||||
if ui.button("Start Proxy").clicked() {
|
|
||||||
self.window_open = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if !self.window_open {
|
|
||||||
let server_addr = self.temp_server_addr.parse::<SocketAddr>().map_err(|_| {
|
|
||||||
self.server_addr_error = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
let client_addr = self.temp_client_addr.parse::<SocketAddr>().map_err(|_| {
|
|
||||||
self.client_addr_error = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
let max_connections = if self.temp_max_connections.is_empty() {
|
|
||||||
Ok(100_000)
|
|
||||||
} else {
|
|
||||||
self.temp_max_connections.parse::<usize>().map_err(|_| {
|
|
||||||
self.max_connections_error = true;
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
if server_addr.is_err() || client_addr.is_err() || max_connections.is_err() {
|
|
||||||
self.window_open = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.config.set_server_addr(server_addr.unwrap());
|
|
||||||
self.config.set_client_addr(client_addr.unwrap());
|
|
||||||
self.config
|
|
||||||
.set_max_connections(if self.temp_max_connections.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(self.temp_max_connections.parse::<usize>().unwrap())
|
|
||||||
});
|
|
||||||
|
|
||||||
self.start_listening(
|
|
||||||
client_addr.unwrap(),
|
|
||||||
server_addr.unwrap(),
|
|
||||||
max_connections.unwrap(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
egui::TopBottomPanel::top("header").show(ctx, |ui| {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("Filter:");
|
|
||||||
if ui.text_edit_singleline(&mut self.filter).changed() {
|
|
||||||
self.context.set_filter(self.filter.clone());
|
|
||||||
|
|
||||||
self.config.set_filter(match self.filter.is_empty() {
|
|
||||||
true => None,
|
|
||||||
false => Some(self.filter.clone()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
ui.menu_button("Packets", |ui| {
|
|
||||||
ui.set_max_width(250.0);
|
|
||||||
ui.set_max_height(400.0);
|
|
||||||
|
|
||||||
ui.text_edit_singleline(&mut self.packet_filter);
|
|
||||||
|
|
||||||
egui::ScrollArea::vertical()
|
|
||||||
.auto_shrink([true, true])
|
|
||||||
.show(ui, |ui| {
|
|
||||||
self.nested_menus(ui);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
egui::SidePanel::left("side_panel")
|
|
||||||
.min_width(150.0)
|
|
||||||
.default_width(250.0)
|
|
||||||
.show(ctx, |ui| {
|
|
||||||
if ui.input(|i| i.key_pressed(egui::Key::ArrowUp)) {
|
|
||||||
self.context.select_previous_packet();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui.input(|i| i.key_pressed(egui::Key::ArrowDown)) {
|
|
||||||
self.context.select_next_packet();
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.heading("Packets");
|
|
||||||
|
|
||||||
let count = self.context.packet_count.read().unwrap();
|
|
||||||
let total = self.context.packets.read().unwrap().len();
|
|
||||||
|
|
||||||
let all_selected = self.selected_packets.values().all(|v| *v);
|
|
||||||
|
|
||||||
if self.filter.is_empty() && all_selected {
|
|
||||||
ui.label(format!("({total})"));
|
|
||||||
} else {
|
|
||||||
ui.label(format!("({count}/{total})"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui.button("Clear").clicked() {
|
|
||||||
self.context.clear();
|
|
||||||
self.buffer = String::new();
|
|
||||||
self.raw_packet = vec![];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui.button("Export").clicked() {
|
|
||||||
if let Some(path) = rfd::FileDialog::new()
|
|
||||||
.add_filter("Text Document", &["txt"])
|
|
||||||
.save_file()
|
|
||||||
{
|
|
||||||
match self.context.save(path) {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(err) => {
|
|
||||||
eprintln!("Failed to save: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
egui::ScrollArea::vertical()
|
|
||||||
.auto_shrink([false, false])
|
|
||||||
.stick_to_bottom(true)
|
|
||||||
.show(ui, |ui| {
|
|
||||||
let mut f = self.context.packets.write().unwrap();
|
|
||||||
|
|
||||||
let f: Vec<&mut Packet> = f
|
|
||||||
.iter_mut()
|
|
||||||
.filter(|p| {
|
|
||||||
let m_packet = MetaPacket {
|
|
||||||
stage: p.stage.clone(),
|
|
||||||
id: p.packet_type,
|
|
||||||
direction: p.direction.clone(),
|
|
||||||
name: p.packet_name.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if !self.selected_packets.contains_key(&m_packet) {
|
|
||||||
self.selected_packets.insert(m_packet.clone(), true);
|
|
||||||
self.config
|
|
||||||
.set_selected_packets(self.selected_packets.clone());
|
|
||||||
self.context
|
|
||||||
.set_selected_packets(self.selected_packets.clone());
|
|
||||||
} else {
|
|
||||||
// if it does exist, check if the names are the same, if not
|
|
||||||
// update the key
|
|
||||||
let (existing, value) =
|
|
||||||
self.selected_packets.get_key_value(&m_packet).unwrap();
|
|
||||||
if existing.name != m_packet.name {
|
|
||||||
let value = *value; // keep the old value
|
|
||||||
self.selected_packets.remove(&m_packet);
|
|
||||||
self.selected_packets.insert(m_packet.clone(), value);
|
|
||||||
self.config
|
|
||||||
.set_selected_packets(self.selected_packets.clone());
|
|
||||||
self.context
|
|
||||||
.set_selected_packets(self.selected_packets.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(selected) = self.selected_packets.get(&m_packet) {
|
|
||||||
if !*selected {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.filter.is_empty() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.packet_name
|
|
||||||
.to_lowercase()
|
|
||||||
.contains(&self.filter.to_lowercase())
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
*self.context.packet_count.write().unwrap() = f.len();
|
|
||||||
|
|
||||||
for packet in f {
|
|
||||||
{
|
|
||||||
let selected = self.context.selected_packet.read().unwrap();
|
|
||||||
if let Some(idx) = *selected {
|
|
||||||
if idx == packet.id {
|
|
||||||
packet.selected(true);
|
|
||||||
self.buffer = packet.get_packet_string(true);
|
|
||||||
self.raw_packet = packet.get_raw_packet();
|
|
||||||
} else {
|
|
||||||
packet.selected(false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
packet.selected(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui.add(packet.clone()).clicked() {
|
|
||||||
self.context.set_selected_packet(packet.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.checkbox(&mut self.view_hex, "Hex View");
|
|
||||||
});
|
|
||||||
|
|
||||||
egui::ScrollArea::both()
|
|
||||||
.auto_shrink([false, false])
|
|
||||||
.show(ui, |ui| {
|
|
||||||
if self.view_hex {
|
|
||||||
hex_view_ui(ui, &self.raw_packet);
|
|
||||||
} else {
|
|
||||||
code_view_ui(ui, &self.buffer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
62
tools/packet_inspector/src/main_cli.rs
Normal file
62
tools/packet_inspector/src/main_cli.rs
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
use clap::Parser;
|
||||||
|
use packet_inspector::Packet;
|
||||||
|
use packet_inspector::Proxy;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use tracing::Level;
|
||||||
|
|
||||||
|
#[derive(Parser, Clone, Debug)]
|
||||||
|
#[clap(author, version, about)]
|
||||||
|
struct CliArgs {
|
||||||
|
/// The socket address to listen for connections on. This is the address clients should connect to
|
||||||
|
listener_addr: SocketAddr,
|
||||||
|
/// The socket address the proxy will connect to. This is the address of the server
|
||||||
|
server_addr: SocketAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_max_level(Level::TRACE)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let args = CliArgs::parse();
|
||||||
|
|
||||||
|
let proxy = Proxy::new(args.listener_addr, args.server_addr);
|
||||||
|
let receiver = proxy.subscribe().await;
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
proxy.run().await?;
|
||||||
|
|
||||||
|
Ok::<(), anyhow::Error>(())
|
||||||
|
});
|
||||||
|
|
||||||
|
// consumer
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Ok(packet) = receiver.recv_async().await {
|
||||||
|
log(&packet);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tokio::signal::ctrl_c().await.unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log(packet: &Packet) {
|
||||||
|
tracing::debug!(
|
||||||
|
"{:?} -> [{:?}] 0x{:0>2X} \"{}\" {:?}",
|
||||||
|
packet.side,
|
||||||
|
packet.state,
|
||||||
|
packet.id,
|
||||||
|
packet.name,
|
||||||
|
truncated(format!("{:?}", packet.data), 512)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn truncated(string: String, max_len: usize) -> String {
|
||||||
|
if string.len() > max_len {
|
||||||
|
format!("{}...", &string[..max_len])
|
||||||
|
} else {
|
||||||
|
string
|
||||||
|
}
|
||||||
|
}
|
44
tools/packet_inspector/src/main_gui.rs
Normal file
44
tools/packet_inspector/src/main_gui.rs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
mod tri_checkbox;
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
mod shared_state;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let native_options = eframe::NativeOptions {
|
||||||
|
icon_data: Some(load_icon()),
|
||||||
|
initial_window_size: Some(egui::Vec2::new(1024.0, 768.0)),
|
||||||
|
decorated: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
eframe::run_native(
|
||||||
|
"Valence Packet Inspector",
|
||||||
|
native_options,
|
||||||
|
Box::new(move |cc| {
|
||||||
|
let gui_app = app::GuiApp::new(cc);
|
||||||
|
|
||||||
|
Box::new(gui_app)
|
||||||
|
}),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_icon() -> eframe::IconData {
|
||||||
|
let (icon_rgba, icon_width, icon_height) = {
|
||||||
|
let icon = include_bytes!("../../../assets/logo-256x256.png");
|
||||||
|
let image = image::load_from_memory(icon)
|
||||||
|
.expect("Failed to open icon path")
|
||||||
|
.into_rgba8();
|
||||||
|
let (width, height) = image.dimensions();
|
||||||
|
let rgba = image.into_raw();
|
||||||
|
(rgba, width, height)
|
||||||
|
};
|
||||||
|
|
||||||
|
eframe::IconData {
|
||||||
|
rgba: icon_rgba,
|
||||||
|
width: icon_width,
|
||||||
|
height: icon_height,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,249 +0,0 @@
|
||||||
use valence::advancement::packet::*;
|
|
||||||
use valence::client::action::*;
|
|
||||||
use valence::client::command::*;
|
|
||||||
use valence::client::custom_payload::*;
|
|
||||||
use valence::client::hand_swing::*;
|
|
||||||
use valence::client::interact_block::*;
|
|
||||||
use valence::client::interact_entity::*;
|
|
||||||
use valence::client::interact_item::*;
|
|
||||||
use valence::client::keepalive::*;
|
|
||||||
use valence::client::movement::*;
|
|
||||||
use valence::client::packet::structure_block::*;
|
|
||||||
use valence::client::packet::*;
|
|
||||||
use valence::client::resource_pack::*;
|
|
||||||
use valence::client::settings::*;
|
|
||||||
use valence::client::status::*;
|
|
||||||
use valence::client::teleport::*;
|
|
||||||
use valence::client::title::*;
|
|
||||||
use valence::entity::packet::*;
|
|
||||||
use valence::instance::packet::*;
|
|
||||||
use valence::inventory::packet::synchronize_recipes::*;
|
|
||||||
use valence::inventory::packet::*;
|
|
||||||
use valence::network::packet::*;
|
|
||||||
use valence::packet_group;
|
|
||||||
use valence::particle::*;
|
|
||||||
use valence::player_list::packet::*;
|
|
||||||
use valence::protocol::packet::boss_bar::*;
|
|
||||||
use valence::protocol::packet::chat::*;
|
|
||||||
use valence::protocol::packet::command::*;
|
|
||||||
use valence::protocol::packet::map::*;
|
|
||||||
use valence::protocol::packet::scoreboard::*;
|
|
||||||
use valence::protocol::packet::sound::*;
|
|
||||||
use valence::protocol::packet::synchronize_tags::*;
|
|
||||||
|
|
||||||
packet_group! {
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub C2sHandshakePacket<'a> {
|
|
||||||
HandshakeC2s<'a>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
packet_group! {
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub S2cStatusPacket<'a> {
|
|
||||||
QueryPongS2c,
|
|
||||||
QueryResponseS2c<'a>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
packet_group! {
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub C2sStatusPacket {
|
|
||||||
QueryPingC2s,
|
|
||||||
QueryRequestC2s,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
packet_group! {
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub S2cLoginPacket<'a> {
|
|
||||||
LoginCompressionS2c,
|
|
||||||
LoginDisconnectS2c<'a>,
|
|
||||||
LoginHelloS2c<'a>,
|
|
||||||
LoginQueryRequestS2c<'a>,
|
|
||||||
LoginSuccessS2c<'a>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
packet_group! {
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub C2sLoginPacket<'a> {
|
|
||||||
LoginHelloC2s<'a>,
|
|
||||||
LoginKeyC2s<'a>,
|
|
||||||
LoginQueryResponseC2s<'a>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
packet_group! {
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub S2cPlayPacket<'a> {
|
|
||||||
AdvancementUpdateS2c<'a>,
|
|
||||||
BlockBreakingProgressS2c,
|
|
||||||
BlockEntityUpdateS2c<'a>,
|
|
||||||
BlockEventS2c,
|
|
||||||
BlockUpdateS2c,
|
|
||||||
BossBarS2c,
|
|
||||||
BundleSplitterS2c,
|
|
||||||
ChatMessageS2c<'a>,
|
|
||||||
ChatSuggestionsS2c<'a>,
|
|
||||||
ChunkBiomeDataS2c<'a>,
|
|
||||||
ChunkDataS2c<'a>,
|
|
||||||
ChunkDeltaUpdateS2c<'a>,
|
|
||||||
ChunkLoadDistanceS2c,
|
|
||||||
ChunkRenderDistanceCenterS2c,
|
|
||||||
ClearTitleS2c,
|
|
||||||
CloseScreenS2c,
|
|
||||||
CommandSuggestionsS2c<'a>,
|
|
||||||
CommandTreeS2c<'a>,
|
|
||||||
CooldownUpdateS2c,
|
|
||||||
CraftFailedResponseS2c<'a>,
|
|
||||||
CustomPayloadS2c<'a>,
|
|
||||||
DamageTiltS2c,
|
|
||||||
DeathMessageS2c<'a>,
|
|
||||||
DifficultyS2c,
|
|
||||||
DisconnectS2c<'a>,
|
|
||||||
EndCombatS2c,
|
|
||||||
EnterCombatS2c,
|
|
||||||
EntitiesDestroyS2c<'a>,
|
|
||||||
EntityAnimationS2c,
|
|
||||||
EntityAttachS2c,
|
|
||||||
EntityAttributesS2c<'a>,
|
|
||||||
EntityDamageS2c,
|
|
||||||
EntityEquipmentUpdateS2c,
|
|
||||||
EntityPassengersSetS2c,
|
|
||||||
EntityPositionS2c,
|
|
||||||
EntitySetHeadYawS2c,
|
|
||||||
EntitySpawnS2c,
|
|
||||||
EntityStatusEffectS2c,
|
|
||||||
EntityStatusS2c,
|
|
||||||
EntityTrackerUpdateS2c<'a>,
|
|
||||||
EntityVelocityUpdateS2c,
|
|
||||||
ExperienceBarUpdateS2c,
|
|
||||||
ExperienceOrbSpawnS2c,
|
|
||||||
ExplosionS2c<'a>,
|
|
||||||
FeaturesS2c<'a>,
|
|
||||||
GameJoinS2c<'a>,
|
|
||||||
GameMessageS2c<'a>,
|
|
||||||
GameStateChangeS2c,
|
|
||||||
HealthUpdateS2c,
|
|
||||||
InventoryS2c<'a>,
|
|
||||||
ItemPickupAnimationS2c,
|
|
||||||
KeepAliveS2c,
|
|
||||||
LightUpdateS2c,
|
|
||||||
LookAtS2c,
|
|
||||||
MapUpdateS2c<'a>,
|
|
||||||
MoveRelativeS2c,
|
|
||||||
NbtQueryResponseS2c,
|
|
||||||
OpenHorseScreenS2c,
|
|
||||||
OpenScreenS2c<'a>,
|
|
||||||
OpenWrittenBookS2c,
|
|
||||||
OverlayMessageS2c<'a>,
|
|
||||||
ParticleS2c<'a>,
|
|
||||||
PlayerAbilitiesS2c,
|
|
||||||
PlayerActionResponseS2c,
|
|
||||||
PlayerListHeaderS2c<'a>,
|
|
||||||
PlayerListS2c<'a>,
|
|
||||||
PlayerPositionLookS2c,
|
|
||||||
PlayerRemoveS2c<'a>,
|
|
||||||
PlayerRespawnS2c<'a>,
|
|
||||||
PlayerSpawnPositionS2c,
|
|
||||||
PlayerSpawnS2c,
|
|
||||||
PlayPingS2c,
|
|
||||||
PlaySoundFromEntityS2c,
|
|
||||||
PlaySoundS2c<'a>,
|
|
||||||
ProfilelessChatMessageS2c<'a>,
|
|
||||||
RemoveEntityStatusEffectS2c,
|
|
||||||
RemoveMessageS2c<'a>,
|
|
||||||
ResourcePackSendS2c<'a>,
|
|
||||||
RotateS2c,
|
|
||||||
RotateAndMoveRelativeS2c,
|
|
||||||
ScoreboardDisplayS2c<'a>,
|
|
||||||
ScoreboardObjectiveUpdateS2c<'a>,
|
|
||||||
ScoreboardPlayerUpdateS2c<'a>,
|
|
||||||
ScreenHandlerPropertyUpdateS2c,
|
|
||||||
ScreenHandlerSlotUpdateS2c<'a>,
|
|
||||||
SelectAdvancementTabS2c<'a>,
|
|
||||||
ServerMetadataS2c<'a>,
|
|
||||||
SetCameraEntityS2c,
|
|
||||||
SetTradeOffersS2c,
|
|
||||||
SignEditorOpenS2c,
|
|
||||||
SimulationDistanceS2c,
|
|
||||||
StatisticsS2c,
|
|
||||||
StopSoundS2c<'a>,
|
|
||||||
SubtitleS2c<'a>,
|
|
||||||
SynchronizeRecipesS2c<'a>,
|
|
||||||
SynchronizeTagsS2c<'a>,
|
|
||||||
TeamS2c<'a>,
|
|
||||||
TitleFadeS2c,
|
|
||||||
TitleS2c<'a>,
|
|
||||||
UnloadChunkS2c,
|
|
||||||
UnlockRecipesS2c<'a>,
|
|
||||||
UpdateSelectedSlotS2c,
|
|
||||||
VehicleMoveS2c,
|
|
||||||
WorldBorderCenterChangedS2c,
|
|
||||||
WorldBorderInitializeS2c,
|
|
||||||
WorldBorderInterpolateSizeS2c,
|
|
||||||
WorldBorderSizeChangedS2c,
|
|
||||||
WorldBorderWarningBlocksChangedS2c,
|
|
||||||
WorldBorderWarningTimeChangedS2c,
|
|
||||||
WorldEventS2c,
|
|
||||||
WorldTimeUpdateS2c,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
packet_group! {
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub C2sPlayPacket<'a> {
|
|
||||||
AdvancementTabC2s<'a>,
|
|
||||||
BoatPaddleStateC2s,
|
|
||||||
BookUpdateC2s<'a>,
|
|
||||||
ButtonClickC2s,
|
|
||||||
ChatMessageC2s<'a>,
|
|
||||||
ClickSlotC2s,
|
|
||||||
ClientCommandC2s,
|
|
||||||
ClientSettingsC2s<'a>,
|
|
||||||
ClientStatusC2s,
|
|
||||||
CloseHandledScreenC2s,
|
|
||||||
CommandExecutionC2s<'a>,
|
|
||||||
CraftRequestC2s<'a>,
|
|
||||||
CreativeInventoryActionC2s,
|
|
||||||
CustomPayloadC2s<'a>,
|
|
||||||
FullC2s,
|
|
||||||
HandSwingC2s,
|
|
||||||
JigsawGeneratingC2s,
|
|
||||||
KeepAliveC2s,
|
|
||||||
LookAndOnGroundC2s,
|
|
||||||
MessageAcknowledgmentC2s,
|
|
||||||
OnGroundOnlyC2s,
|
|
||||||
PickFromInventoryC2s,
|
|
||||||
PlayerActionC2s,
|
|
||||||
PlayerInputC2s,
|
|
||||||
PlayerInteractBlockC2s,
|
|
||||||
PlayerInteractEntityC2s,
|
|
||||||
PlayerInteractItemC2s,
|
|
||||||
PlayerSessionC2s<'a>,
|
|
||||||
PlayPongC2s,
|
|
||||||
PositionAndOnGroundC2s,
|
|
||||||
QueryBlockNbtC2s,
|
|
||||||
QueryEntityNbtC2s,
|
|
||||||
RecipeBookDataC2s<'a>,
|
|
||||||
RecipeCategoryOptionsC2s,
|
|
||||||
RenameItemC2s<'a>,
|
|
||||||
RequestCommandCompletionsC2s<'a>,
|
|
||||||
ResourcePackStatusC2s,
|
|
||||||
SelectMerchantTradeC2s,
|
|
||||||
SpectatorTeleportC2s,
|
|
||||||
TeleportConfirmC2s,
|
|
||||||
UpdateBeaconC2s,
|
|
||||||
UpdateCommandBlockC2s<'a>,
|
|
||||||
UpdateCommandBlockMinecartC2s<'a>,
|
|
||||||
UpdateDifficultyC2s,
|
|
||||||
UpdateDifficultyLockC2s,
|
|
||||||
UpdateJigsawC2s<'a>,
|
|
||||||
UpdatePlayerAbilitiesC2s,
|
|
||||||
UpdateSelectedSlotC2s,
|
|
||||||
UpdateSignC2s<'a>,
|
|
||||||
UpdateStructureBlockC2s<'a>,
|
|
||||||
VehicleMoveC2s,
|
|
||||||
}
|
|
||||||
}
|
|
195
tools/packet_inspector/src/packet_io.rs
Normal file
195
tools/packet_inspector/src/packet_io.rs
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
use anyhow::ensure;
|
||||||
|
use bytes::BufMut;
|
||||||
|
use bytes::BytesMut;
|
||||||
|
use std::io;
|
||||||
|
use std::io::ErrorKind;
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
use valence::__private::VarInt;
|
||||||
|
use valence::protocol::decode::{PacketDecoder, PacketFrame};
|
||||||
|
use valence::protocol::encode::PacketEncoder;
|
||||||
|
use valence::protocol::Encode;
|
||||||
|
use valence::protocol::MAX_PACKET_SIZE;
|
||||||
|
|
||||||
|
pub(crate) struct PacketIoReader {
|
||||||
|
reader: tokio::io::ReadHalf<tokio::net::TcpStream>,
|
||||||
|
dec: PacketDecoder,
|
||||||
|
threshold: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PacketIoReader {
|
||||||
|
pub(crate) async fn recv_packet_raw(&mut self) -> anyhow::Result<PacketFrame> {
|
||||||
|
loop {
|
||||||
|
if let Some(frame) = self.dec.try_next_packet()? {
|
||||||
|
// self.logger
|
||||||
|
// .log("Unknown".to_string(), self.direction.clone(), frame.clone());
|
||||||
|
|
||||||
|
return Ok(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.dec.reserve(READ_BUF_SIZE);
|
||||||
|
let mut buf = self.dec.take_capacity();
|
||||||
|
|
||||||
|
if self.reader.read_buf(&mut buf).await? == 0 {
|
||||||
|
return Err(io::Error::from(ErrorKind::UnexpectedEof).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should always be an O(1) unsplit because we reserved space earlier and
|
||||||
|
// the call to `read_buf` shouldn't have grown the allocation.
|
||||||
|
self.dec.queue_bytes(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) fn set_compression(&mut self, threshold: Option<u32>) {
|
||||||
|
self.threshold = threshold;
|
||||||
|
self.dec.set_compression(threshold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct PacketIoWriter {
|
||||||
|
writer: tokio::io::WriteHalf<tokio::net::TcpStream>,
|
||||||
|
enc: PacketEncoder,
|
||||||
|
threshold: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PacketIoWriter {
|
||||||
|
/*
|
||||||
|
No | Packet Length | VarInt | Length of (Data Length) + Compressed length of (Packet ID + Data)
|
||||||
|
No | Data Length | VarInt | Length of uncompressed (Packet ID + Data) or 0
|
||||||
|
Yes | Packet ID | VarInt | zlib compressed packet ID (see the sections below)
|
||||||
|
Yes | Data | Byte Array | zlib compressed packet data (see the sections below)
|
||||||
|
*/
|
||||||
|
pub(crate) async fn send_packet_raw(&mut self, frame: &PacketFrame) -> anyhow::Result<()> {
|
||||||
|
let id_varint = VarInt(frame.id);
|
||||||
|
let id_buf = varint_to_bytes(id_varint);
|
||||||
|
|
||||||
|
let mut uncompressed_packet = BytesMut::new();
|
||||||
|
uncompressed_packet.extend_from_slice(&id_buf);
|
||||||
|
uncompressed_packet.extend_from_slice(&frame.body);
|
||||||
|
let uncompressed_packet_length = uncompressed_packet.len();
|
||||||
|
let uncompressed_packet_length_varint = VarInt(uncompressed_packet_length as i32);
|
||||||
|
|
||||||
|
if let Some(threshold) = self.threshold {
|
||||||
|
if uncompressed_packet_length > threshold as usize {
|
||||||
|
use flate2::{bufread::ZlibEncoder, Compression};
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
let mut z = ZlibEncoder::new(&uncompressed_packet[..], Compression::new(4));
|
||||||
|
let mut compressed = Vec::new();
|
||||||
|
|
||||||
|
let data_len_size = uncompressed_packet_length_varint.written_size();
|
||||||
|
|
||||||
|
let packet_len = data_len_size + z.read_to_end(&mut compressed)?;
|
||||||
|
|
||||||
|
ensure!(
|
||||||
|
packet_len <= MAX_PACKET_SIZE as usize,
|
||||||
|
"packet exceeds maximum length"
|
||||||
|
);
|
||||||
|
|
||||||
|
drop(z);
|
||||||
|
|
||||||
|
self.enc
|
||||||
|
.append_bytes(&varint_to_bytes(VarInt(packet_len as i32)));
|
||||||
|
|
||||||
|
self.enc
|
||||||
|
.append_bytes(&varint_to_bytes(uncompressed_packet_length_varint));
|
||||||
|
|
||||||
|
self.enc.append_bytes(&compressed);
|
||||||
|
|
||||||
|
let bytes = self.enc.take();
|
||||||
|
|
||||||
|
self.writer.write_all(&bytes).await?;
|
||||||
|
self.writer.flush().await?;
|
||||||
|
|
||||||
|
// now we need to compress the packet.
|
||||||
|
} else {
|
||||||
|
// no need to compress, but we do need to inject a zero
|
||||||
|
let empty = VarInt(0);
|
||||||
|
|
||||||
|
let data_len_size = empty.written_size();
|
||||||
|
let packet_len = data_len_size + uncompressed_packet_length;
|
||||||
|
|
||||||
|
self.enc
|
||||||
|
.append_bytes(&varint_to_bytes(VarInt(packet_len as i32)));
|
||||||
|
self.enc.append_bytes(&varint_to_bytes(empty));
|
||||||
|
self.enc.append_bytes(&uncompressed_packet);
|
||||||
|
let bytes = self.enc.take();
|
||||||
|
self.writer.write_all(&bytes).await?;
|
||||||
|
self.writer.flush().await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let length = varint_to_bytes(VarInt(uncompressed_packet_length as i32));
|
||||||
|
|
||||||
|
// the frame should be uncompressed at this point.
|
||||||
|
self.enc.append_bytes(&length);
|
||||||
|
self.enc.append_bytes(&uncompressed_packet);
|
||||||
|
|
||||||
|
let bytes = self.enc.take();
|
||||||
|
|
||||||
|
self.writer.write_all(&bytes).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) fn set_compression(&mut self, threshold: Option<u32>) {
|
||||||
|
self.threshold = threshold;
|
||||||
|
self.enc.set_compression(threshold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct PacketIo {
|
||||||
|
stream: TcpStream,
|
||||||
|
enc: PacketEncoder,
|
||||||
|
dec: PacketDecoder,
|
||||||
|
threshold: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const READ_BUF_SIZE: usize = 1024;
|
||||||
|
|
||||||
|
impl PacketIo {
|
||||||
|
pub(crate) fn new(stream: TcpStream) -> Self {
|
||||||
|
Self {
|
||||||
|
stream,
|
||||||
|
enc: PacketEncoder::new(),
|
||||||
|
dec: PacketDecoder::new(),
|
||||||
|
threshold: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn split(self) -> (PacketIoReader, PacketIoWriter) {
|
||||||
|
let (reader, writer) = tokio::io::split(self.stream);
|
||||||
|
|
||||||
|
(
|
||||||
|
PacketIoReader {
|
||||||
|
reader,
|
||||||
|
dec: self.dec,
|
||||||
|
threshold: self.threshold,
|
||||||
|
},
|
||||||
|
PacketIoWriter {
|
||||||
|
writer,
|
||||||
|
enc: self.enc,
|
||||||
|
threshold: self.threshold,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) async fn set_compression(&mut self, threshold: Option<u32>) {
|
||||||
|
self.threshold = threshold;
|
||||||
|
self.enc.set_compression(threshold);
|
||||||
|
self.dec.set_compression(threshold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn varint_to_bytes(i: VarInt) -> BytesMut {
|
||||||
|
let mut buf = BytesMut::new();
|
||||||
|
let mut writer = (&mut buf).writer();
|
||||||
|
i.encode(&mut writer).unwrap();
|
||||||
|
|
||||||
|
buf
|
||||||
|
}
|
132
tools/packet_inspector/src/packet_registry.rs
Normal file
132
tools/packet_inspector/src/packet_registry.rs
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
use std::{
|
||||||
|
hash::{Hash, Hasher},
|
||||||
|
sync::RwLock,
|
||||||
|
};
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
use valence::protocol::decode::PacketFrame;
|
||||||
|
|
||||||
|
pub struct PacketRegistry {
|
||||||
|
packets: RwLock<Vec<Packet>>,
|
||||||
|
receiver: flume::Receiver<Packet>,
|
||||||
|
sender: flume::Sender<Packet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
impl PacketRegistry {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let (sender, receiver) = flume::unbounded::<Packet>();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
packets: RwLock::new(Vec::new()),
|
||||||
|
receiver,
|
||||||
|
sender,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subscribe(&self) -> flume::Receiver<Packet> {
|
||||||
|
self.receiver.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(&self, packet: Packet) {
|
||||||
|
self.packets.write().unwrap().push(packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
// register_all(takes an array of packets)
|
||||||
|
pub fn register_all(&self, packets: &[Packet]) {
|
||||||
|
self.packets.write().unwrap().extend_from_slice(packets);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_specific_packet(&self, side: PacketSide, state: PacketState, packet_id: i32) -> Packet {
|
||||||
|
let time = match OffsetDateTime::now_local() {
|
||||||
|
Ok(time) => time,
|
||||||
|
Err(_) => OffsetDateTime::now_utc(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.packets
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.find(|packet| packet.id == packet_id && packet.side == side && packet.state == state)
|
||||||
|
.unwrap_or(&Packet {
|
||||||
|
side,
|
||||||
|
state,
|
||||||
|
id: packet_id,
|
||||||
|
timestamp: Some(time),
|
||||||
|
name: "Unknown Packet",
|
||||||
|
data: None,
|
||||||
|
})
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn process(
|
||||||
|
&self,
|
||||||
|
side: PacketSide,
|
||||||
|
state: PacketState,
|
||||||
|
threshold: Option<u32>,
|
||||||
|
packet: &PacketFrame,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let mut p = self.get_specific_packet(side, state, packet.id);
|
||||||
|
let time = match OffsetDateTime::now_local() {
|
||||||
|
Ok(time) => time,
|
||||||
|
Err(_) => OffsetDateTime::now_utc(),
|
||||||
|
};
|
||||||
|
|
||||||
|
p.data = Some(packet.body.clone().freeze());
|
||||||
|
p.timestamp = Some(time);
|
||||||
|
|
||||||
|
// store in received_packets
|
||||||
|
self.sender.send_async(p).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
pub struct Packet {
|
||||||
|
pub side: PacketSide,
|
||||||
|
pub state: PacketState,
|
||||||
|
pub id: i32,
|
||||||
|
#[cfg_attr(feature = "serde", serde[skip])]
|
||||||
|
pub timestamp: Option<OffsetDateTime>,
|
||||||
|
#[cfg_attr(feature = "serde", serde[skip])]
|
||||||
|
pub name: &'static str,
|
||||||
|
/// Uncompressed packet data
|
||||||
|
#[cfg_attr(feature = "serde", serde[skip])]
|
||||||
|
pub data: Option<Bytes>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Packet {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.side == other.side
|
||||||
|
&& self.state == other.state
|
||||||
|
&& self.id == other.id
|
||||||
|
&& self.data == other.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for Packet {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.id.hash(state);
|
||||||
|
self.side.hash(state);
|
||||||
|
self.state.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
pub enum PacketState {
|
||||||
|
Handshaking,
|
||||||
|
Status,
|
||||||
|
Login,
|
||||||
|
Play,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
pub enum PacketSide {
|
||||||
|
Clientbound,
|
||||||
|
Serverbound,
|
||||||
|
}
|
|
@ -1,156 +0,0 @@
|
||||||
use eframe::epaint::{PathShape, RectShape};
|
|
||||||
use egui::{
|
|
||||||
Color32, Pos2, Rect, Response, Rgba, Rounding, Sense, Shape, Stroke, TextStyle, Ui, Vec2,
|
|
||||||
Widget, WidgetText,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use time::OffsetDateTime;
|
|
||||||
|
|
||||||
use crate::context::Packet;
|
|
||||||
|
|
||||||
pub fn systemtime_strftime(odt: OffsetDateTime) -> String {
|
|
||||||
let hour = odt.hour();
|
|
||||||
let minute = odt.minute();
|
|
||||||
let second = odt.second();
|
|
||||||
let millis = odt.millisecond();
|
|
||||||
|
|
||||||
format!("{hour:0>2}:{minute:0>2}:{second:0>2}.{millis:0>4}")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, PartialOrd, Eq, Ord, Serialize, Deserialize)]
|
|
||||||
pub enum PacketDirection {
|
|
||||||
ClientToServer,
|
|
||||||
ServerToClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PacketDirection {
|
|
||||||
fn get_shape(&self, outer_rect: &Rect) -> PathShape {
|
|
||||||
let rect = Rect::from_min_size(
|
|
||||||
Pos2 {
|
|
||||||
x: outer_rect.left() + 6.0,
|
|
||||||
y: outer_rect.top() + 8.0,
|
|
||||||
},
|
|
||||||
Vec2 { x: 8.0, y: 8.0 },
|
|
||||||
);
|
|
||||||
|
|
||||||
let color = match self {
|
|
||||||
PacketDirection::ServerToClient => Rgba::from_rgb(255.0, 0.0, 0.0),
|
|
||||||
PacketDirection::ClientToServer => Rgba::from_rgb(0.0, 255.0, 0.0),
|
|
||||||
};
|
|
||||||
|
|
||||||
let points = match self {
|
|
||||||
PacketDirection::ServerToClient => vec![
|
|
||||||
Pos2 {
|
|
||||||
x: rect.left() + (rect.width() / 2.0),
|
|
||||||
y: rect.top() + rect.height(),
|
|
||||||
},
|
|
||||||
Pos2 {
|
|
||||||
x: rect.left() + 0.0,
|
|
||||||
y: rect.top(),
|
|
||||||
},
|
|
||||||
Pos2 {
|
|
||||||
x: rect.left() + rect.width(),
|
|
||||||
y: rect.top(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
PacketDirection::ClientToServer => vec![
|
|
||||||
Pos2 {
|
|
||||||
x: rect.left() + (rect.width() / 2.0),
|
|
||||||
y: rect.top() + 0.0,
|
|
||||||
},
|
|
||||||
Pos2 {
|
|
||||||
x: rect.left() + 0.0,
|
|
||||||
y: rect.top() + rect.height(),
|
|
||||||
},
|
|
||||||
Pos2 {
|
|
||||||
x: rect.left() + rect.width(),
|
|
||||||
y: rect.top() + rect.height(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut shape = PathShape::closed_line(points, Stroke::new(2.0, color));
|
|
||||||
shape.fill = color.into();
|
|
||||||
|
|
||||||
shape
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for Packet {
|
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
|
||||||
let (mut rect, response) = ui.allocate_at_least(
|
|
||||||
Vec2 {
|
|
||||||
x: ui.available_width(),
|
|
||||||
y: 24.0,
|
|
||||||
},
|
|
||||||
Sense::click(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let fill = match self.selected {
|
|
||||||
true => Rgba::from_rgba_premultiplied(0.3, 0.3, 0.3, 0.4),
|
|
||||||
false => Rgba::from_rgba_premultiplied(0.0, 0.0, 0.0, 0.0),
|
|
||||||
};
|
|
||||||
|
|
||||||
let text_color: Color32 = match self.selected {
|
|
||||||
true => Rgba::from_rgba_premultiplied(0.0, 0.0, 0.0, 1.0).into(),
|
|
||||||
false => ui.visuals().strong_text_color(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if ui.is_rect_visible(rect) {
|
|
||||||
ui.painter().add(Shape::Rect(RectShape {
|
|
||||||
rect,
|
|
||||||
rounding: Rounding::none(),
|
|
||||||
fill: fill.into(),
|
|
||||||
stroke: Stroke::new(1.0, Rgba::BLACK),
|
|
||||||
}));
|
|
||||||
|
|
||||||
let shape = self.direction.get_shape(&rect);
|
|
||||||
ui.painter().add(Shape::Path(shape));
|
|
||||||
|
|
||||||
let identifier: WidgetText = format!("0x{:0>2X?}", self.packet_type).into();
|
|
||||||
|
|
||||||
let identifier =
|
|
||||||
identifier.into_galley(ui, Some(false), rect.width() - 21.0, TextStyle::Button);
|
|
||||||
|
|
||||||
let label: WidgetText = self.packet_name.into();
|
|
||||||
let label = label.into_galley(ui, Some(false), rect.width() - 60.0, TextStyle::Button);
|
|
||||||
|
|
||||||
let timestamp: WidgetText = systemtime_strftime(self.created_at).into();
|
|
||||||
let timestamp =
|
|
||||||
timestamp.into_galley(ui, Some(false), rect.width() - 60.0, TextStyle::Button);
|
|
||||||
|
|
||||||
identifier.paint_with_fallback_color(
|
|
||||||
ui.painter(),
|
|
||||||
Pos2 {
|
|
||||||
x: rect.left() + 21.0,
|
|
||||||
y: rect.top() + 6.0,
|
|
||||||
},
|
|
||||||
ui.visuals().weak_text_color(),
|
|
||||||
);
|
|
||||||
|
|
||||||
rect.set_width(rect.width() - 5.0);
|
|
||||||
|
|
||||||
let label_width = label.size().x + 50.0;
|
|
||||||
|
|
||||||
label.paint_with_fallback_color(
|
|
||||||
&ui.painter().with_clip_rect(rect),
|
|
||||||
Pos2 {
|
|
||||||
x: rect.left() + 55.0,
|
|
||||||
y: rect.top() + 6.0,
|
|
||||||
},
|
|
||||||
text_color,
|
|
||||||
);
|
|
||||||
|
|
||||||
timestamp.paint_with_fallback_color(
|
|
||||||
&ui.painter().with_clip_rect(rect),
|
|
||||||
Pos2 {
|
|
||||||
x: rect.left() + label_width + 8.0,
|
|
||||||
y: rect.top() + 6.0,
|
|
||||||
},
|
|
||||||
ui.visuals().weak_text_color(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
response
|
|
||||||
}
|
|
||||||
}
|
|
132
tools/packet_inspector/src/shared_state.rs
Normal file
132
tools/packet_inspector/src/shared_state.rs
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
#![allow(clippy::mutable_key_type)]
|
||||||
|
|
||||||
|
use egui::Context;
|
||||||
|
use packet_inspector::Packet;
|
||||||
|
use std::{collections::HashMap, sync::RwLock};
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct PacketFilter {
|
||||||
|
inner: HashMap<Packet, bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PacketFilter {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let mut inner = HashMap::new();
|
||||||
|
|
||||||
|
for p in packet_inspector::STD_PACKETS.iter() {
|
||||||
|
inner.insert(p.clone(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self { inner }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, packet: &Packet) -> Option<bool> {
|
||||||
|
self.inner
|
||||||
|
.iter()
|
||||||
|
.find(|(k, _)| k.id == packet.id && k.side == packet.side && k.state == packet.state)
|
||||||
|
.map(|(_, v)| v)
|
||||||
|
.copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&mut self, packet: Packet, value: bool) {
|
||||||
|
self.inner.insert(packet, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = (&Packet, &bool)> {
|
||||||
|
self.inner.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter_mut(&mut self) -> impl Iterator<Item = (&Packet, &mut bool)> {
|
||||||
|
self.inner.iter_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Event {
|
||||||
|
StartListening,
|
||||||
|
StopListening,
|
||||||
|
PacketReceived,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct SharedState {
|
||||||
|
pub listener_addr: String,
|
||||||
|
pub server_addr: String,
|
||||||
|
pub autostart: bool,
|
||||||
|
pub packet_filter: PacketFilter,
|
||||||
|
pub packet_search: String,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub is_listening: bool,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub selected_packet: Option<usize>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub update_scroll: bool,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub packets: RwLock<Vec<Packet>>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub(super) receiver: Option<flume::Receiver<Event>>,
|
||||||
|
#[serde(skip)]
|
||||||
|
sender: Option<flume::Sender<Event>>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub ctx: Option<Context>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SharedState {
|
||||||
|
fn default() -> Self {
|
||||||
|
let (sender, receiver) = flume::unbounded();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
listener_addr: "127.0.0.1:25566".to_string(),
|
||||||
|
server_addr: "127.0.0.1:25565".to_string(),
|
||||||
|
autostart: false,
|
||||||
|
is_listening: false,
|
||||||
|
packet_search: String::new(),
|
||||||
|
packet_filter: PacketFilter::new(),
|
||||||
|
selected_packet: None,
|
||||||
|
update_scroll: false,
|
||||||
|
packets: RwLock::new(Vec::new()),
|
||||||
|
receiver: Some(receiver),
|
||||||
|
sender: Some(sender),
|
||||||
|
ctx: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
impl SharedState {
|
||||||
|
pub fn new(ctx: Context) -> Self {
|
||||||
|
Self {
|
||||||
|
ctx: Some(ctx),
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub(super) fn merge(mut self, other: Self) -> Self {
|
||||||
|
self.ctx = other.ctx;
|
||||||
|
self.sender = other.sender;
|
||||||
|
self.receiver = other.receiver;
|
||||||
|
|
||||||
|
// make a backup of self.packet_filter
|
||||||
|
|
||||||
|
let mut packet_filter = PacketFilter::new();
|
||||||
|
// iterate over packet_inspector::STD_PACKETS
|
||||||
|
for p in packet_inspector::STD_PACKETS.iter() {
|
||||||
|
// if the packet is in the current packet_filter
|
||||||
|
if let Some(v) = self.packet_filter.get(p) {
|
||||||
|
// insert it into packet_filter
|
||||||
|
packet_filter.insert(p.clone(), v);
|
||||||
|
} else {
|
||||||
|
// otherwise insert it into packet_filter with a default value of true
|
||||||
|
packet_filter.insert(p.clone(), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.packet_filter = packet_filter;
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_event(&self, event: Event) {
|
||||||
|
if let Some(sender) = &self.sender {
|
||||||
|
sender.send(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,71 +0,0 @@
|
||||||
use std::io::ErrorKind;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use bytes::BytesMut;
|
|
||||||
use time::OffsetDateTime;
|
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
||||||
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
|
|
||||||
use valence::protocol::decode::{decode_packet, PacketDecoder};
|
|
||||||
use valence::protocol::encode::PacketEncoder;
|
|
||||||
use valence::protocol::Packet as ValencePacket;
|
|
||||||
|
|
||||||
use crate::context::{Context, Packet, Stage};
|
|
||||||
use crate::packet_widget::PacketDirection;
|
|
||||||
|
|
||||||
pub struct State {
|
|
||||||
pub direction: PacketDirection,
|
|
||||||
pub context: Arc<Context>,
|
|
||||||
pub enc: PacketEncoder,
|
|
||||||
pub dec: PacketDecoder,
|
|
||||||
pub frame: BytesMut,
|
|
||||||
pub read: OwnedReadHalf,
|
|
||||||
pub write: OwnedWriteHalf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl State {
|
|
||||||
pub async fn rw_packet<'a, P>(&'a mut self, stage: Stage) -> anyhow::Result<P>
|
|
||||||
where
|
|
||||||
P: ValencePacket<'a>,
|
|
||||||
{
|
|
||||||
loop {
|
|
||||||
if let Some(frame) = self.dec.try_next_packet()? {
|
|
||||||
self.frame = frame;
|
|
||||||
|
|
||||||
let pkt: P = decode_packet(&self.frame)?;
|
|
||||||
|
|
||||||
self.enc.append_packet(&pkt)?;
|
|
||||||
|
|
||||||
let bytes = self.enc.take();
|
|
||||||
self.write.write_all(&bytes).await?;
|
|
||||||
|
|
||||||
let time = match OffsetDateTime::now_local() {
|
|
||||||
Ok(time) => time,
|
|
||||||
Err(_) => OffsetDateTime::now_utc(),
|
|
||||||
};
|
|
||||||
|
|
||||||
self.context.add(Packet {
|
|
||||||
id: 0, // updated when added to context
|
|
||||||
direction: self.direction.clone(),
|
|
||||||
compression_threshold: self.dec.compression(),
|
|
||||||
packet_data: bytes.to_vec(),
|
|
||||||
stage,
|
|
||||||
created_at: time,
|
|
||||||
selected: false,
|
|
||||||
packet_type: pkt.packet_id(),
|
|
||||||
packet_name: pkt.packet_name().to_string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok(pkt);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.dec.reserve(4096);
|
|
||||||
let mut buf = self.dec.take_capacity();
|
|
||||||
|
|
||||||
if self.read.read_buf(&mut buf).await? == 0 {
|
|
||||||
return Err(std::io::Error::from(ErrorKind::UnexpectedEof).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
self.dec.queue_bytes(buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,513 +0,0 @@
|
||||||
// From: https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/syntax_highlighting.rs
|
|
||||||
|
|
||||||
use egui::text::LayoutJob;
|
|
||||||
|
|
||||||
/// View some code with syntax highlighting and selection.
|
|
||||||
pub fn code_view_ui(ui: &mut egui::Ui, mut code: &str) {
|
|
||||||
let language = "rs";
|
|
||||||
let theme = CodeTheme::from_memory(ui.ctx());
|
|
||||||
|
|
||||||
let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
|
|
||||||
let mut layout_job = highlight(ui.ctx(), &theme, string, language);
|
|
||||||
layout_job.wrap.max_width = wrap_width; // no wrapping
|
|
||||||
ui.fonts(|f| f.layout_job(layout_job))
|
|
||||||
};
|
|
||||||
|
|
||||||
ui.add(
|
|
||||||
egui::TextEdit::multiline(&mut code)
|
|
||||||
.font(egui::TextStyle::Monospace) // for cursor height
|
|
||||||
.code_editor()
|
|
||||||
.desired_width(ui.available_width())
|
|
||||||
.desired_rows(24)
|
|
||||||
.lock_focus(true)
|
|
||||||
.layouter(&mut layouter),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Memoized Code highlighting
|
|
||||||
pub fn highlight(ctx: &egui::Context, theme: &CodeTheme, code: &str, language: &str) -> LayoutJob {
|
|
||||||
impl egui::util::cache::ComputerMut<(&CodeTheme, &str, &str), LayoutJob> for Highlighter {
|
|
||||||
fn compute(&mut self, (theme, code, lang): (&CodeTheme, &str, &str)) -> LayoutJob {
|
|
||||||
self.highlight(theme, code, lang)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type HighlightCache = egui::util::cache::FrameCache<LayoutJob, Highlighter>;
|
|
||||||
|
|
||||||
ctx.memory_mut(|mem| {
|
|
||||||
mem.caches
|
|
||||||
.cache::<HighlightCache>()
|
|
||||||
.get((theme, code, language))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[cfg(not(feature = "syntect"))]
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
|
||||||
#[derive(enum_map::Enum)]
|
|
||||||
enum TokenType {
|
|
||||||
Comment,
|
|
||||||
Keyword,
|
|
||||||
Literal,
|
|
||||||
StringLiteral,
|
|
||||||
Punctuation,
|
|
||||||
Whitespace,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "syntect")]
|
|
||||||
#[derive(Clone, Copy, Hash, PartialEq)]
|
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
|
||||||
#[allow(unused)]
|
|
||||||
enum SyntectTheme {
|
|
||||||
Base16EightiesDark,
|
|
||||||
Base16MochaDark,
|
|
||||||
Base16OceanDark,
|
|
||||||
Base16OceanLight,
|
|
||||||
InspiredGitHub,
|
|
||||||
SolarizedDark,
|
|
||||||
SolarizedLight,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
#[cfg(feature = "syntect")]
|
|
||||||
impl SyntectTheme {
|
|
||||||
fn all() -> impl ExactSizeIterator<Item = Self> {
|
|
||||||
[
|
|
||||||
Self::Base16EightiesDark,
|
|
||||||
Self::Base16MochaDark,
|
|
||||||
Self::Base16OceanDark,
|
|
||||||
Self::Base16OceanLight,
|
|
||||||
Self::InspiredGitHub,
|
|
||||||
Self::SolarizedDark,
|
|
||||||
Self::SolarizedLight,
|
|
||||||
]
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Base16EightiesDark => "Base16 Eighties (dark)",
|
|
||||||
Self::Base16MochaDark => "Base16 Mocha (dark)",
|
|
||||||
Self::Base16OceanDark => "Base16 Ocean (dark)",
|
|
||||||
Self::Base16OceanLight => "Base16 Ocean (light)",
|
|
||||||
Self::InspiredGitHub => "InspiredGitHub (light)",
|
|
||||||
Self::SolarizedDark => "Solarized (dark)",
|
|
||||||
Self::SolarizedLight => "Solarized (light)",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn syntect_key_name(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Base16EightiesDark => "base16-eighties.dark",
|
|
||||||
Self::Base16MochaDark => "base16-mocha.dark",
|
|
||||||
Self::Base16OceanDark => "base16-ocean.dark",
|
|
||||||
Self::Base16OceanLight => "base16-ocean.light",
|
|
||||||
Self::InspiredGitHub => "InspiredGitHub",
|
|
||||||
Self::SolarizedDark => "Solarized (dark)",
|
|
||||||
Self::SolarizedLight => "Solarized (light)",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_dark(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
Self::Base16EightiesDark
|
|
||||||
| Self::Base16MochaDark
|
|
||||||
| Self::Base16OceanDark
|
|
||||||
| Self::SolarizedDark => true,
|
|
||||||
|
|
||||||
Self::Base16OceanLight | Self::InspiredGitHub | Self::SolarizedLight => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Hash, PartialEq)]
|
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
|
||||||
#[cfg_attr(feature = "serde", serde(default))]
|
|
||||||
pub struct CodeTheme {
|
|
||||||
dark_mode: bool,
|
|
||||||
|
|
||||||
#[cfg(feature = "syntect")]
|
|
||||||
syntect_theme: SyntectTheme,
|
|
||||||
|
|
||||||
#[cfg(not(feature = "syntect"))]
|
|
||||||
formats: enum_map::EnumMap<TokenType, egui::TextFormat>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for CodeTheme {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::dark()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
impl CodeTheme {
|
|
||||||
pub fn from_style(style: &egui::Style) -> Self {
|
|
||||||
if style.visuals.dark_mode {
|
|
||||||
Self::dark()
|
|
||||||
} else {
|
|
||||||
Self::light()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_memory(ctx: &egui::Context) -> Self {
|
|
||||||
if ctx.style().visuals.dark_mode {
|
|
||||||
ctx.data_mut(|d| {
|
|
||||||
d.get_persisted(egui::Id::new("dark"))
|
|
||||||
.unwrap_or_else(CodeTheme::dark)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
ctx.data_mut(|d| {
|
|
||||||
d.get_persisted(egui::Id::new("light"))
|
|
||||||
.unwrap_or_else(CodeTheme::light)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn store_in_memory(self, ctx: &egui::Context) {
|
|
||||||
if self.dark_mode {
|
|
||||||
ctx.data_mut(|d| d.insert_persisted(egui::Id::new("dark"), self));
|
|
||||||
} else {
|
|
||||||
ctx.data_mut(|d| d.insert_persisted(egui::Id::new("light"), self));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
#[cfg(feature = "syntect")]
|
|
||||||
impl CodeTheme {
|
|
||||||
pub fn dark() -> Self {
|
|
||||||
Self {
|
|
||||||
dark_mode: true,
|
|
||||||
syntect_theme: SyntectTheme::SolarizedDark,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn light() -> Self {
|
|
||||||
Self {
|
|
||||||
dark_mode: false,
|
|
||||||
syntect_theme: SyntectTheme::SolarizedLight,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
|
||||||
egui::widgets::global_dark_light_mode_buttons(ui);
|
|
||||||
|
|
||||||
for theme in SyntectTheme::all() {
|
|
||||||
if theme.is_dark() == self.dark_mode {
|
|
||||||
ui.radio_value(&mut self.syntect_theme, theme, theme.name());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "syntect"))]
|
|
||||||
impl CodeTheme {
|
|
||||||
pub fn dark() -> Self {
|
|
||||||
let font_id = egui::FontId::monospace(10.0);
|
|
||||||
use egui::{Color32, TextFormat};
|
|
||||||
Self {
|
|
||||||
dark_mode: true,
|
|
||||||
formats: enum_map::enum_map![
|
|
||||||
TokenType::Comment => TextFormat::simple(font_id.clone(), Color32::from_gray(120)),
|
|
||||||
TokenType::Keyword => TextFormat::simple(font_id.clone(), Color32::from_rgb(255, 100, 100)),
|
|
||||||
TokenType::Literal => TextFormat::simple(font_id.clone(), Color32::from_rgb(87, 165, 171)),
|
|
||||||
TokenType::StringLiteral => TextFormat::simple(font_id.clone(), Color32::from_rgb(109, 147, 226)),
|
|
||||||
TokenType::Punctuation => TextFormat::simple(font_id.clone(), Color32::LIGHT_GRAY),
|
|
||||||
TokenType::Whitespace => TextFormat::simple(font_id.clone(), Color32::TRANSPARENT),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn light() -> Self {
|
|
||||||
let font_id = egui::FontId::monospace(10.0);
|
|
||||||
use egui::{Color32, TextFormat};
|
|
||||||
Self {
|
|
||||||
dark_mode: false,
|
|
||||||
#[cfg(not(feature = "syntect"))]
|
|
||||||
formats: enum_map::enum_map![
|
|
||||||
TokenType::Comment => TextFormat::simple(font_id.clone(), Color32::GRAY),
|
|
||||||
TokenType::Keyword => TextFormat::simple(font_id.clone(), Color32::from_rgb(235, 0, 0)),
|
|
||||||
TokenType::Literal => TextFormat::simple(font_id.clone(), Color32::from_rgb(153, 134, 255)),
|
|
||||||
TokenType::StringLiteral => TextFormat::simple(font_id.clone(), Color32::from_rgb(37, 203, 105)),
|
|
||||||
TokenType::Punctuation => TextFormat::simple(font_id.clone(), Color32::DARK_GRAY),
|
|
||||||
TokenType::Whitespace => TextFormat::simple(font_id.clone(), Color32::TRANSPARENT),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
|
||||||
ui.horizontal_top(|ui| {
|
|
||||||
let selected_id = egui::Id::null();
|
|
||||||
let mut selected_tt: TokenType =
|
|
||||||
ui.data_mut(|d| *d.get_persisted_mut_or(selected_id, TokenType::Comment));
|
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.set_width(150.0);
|
|
||||||
egui::widgets::global_dark_light_mode_buttons(ui);
|
|
||||||
|
|
||||||
ui.add_space(8.0);
|
|
||||||
ui.separator();
|
|
||||||
ui.add_space(8.0);
|
|
||||||
|
|
||||||
ui.scope(|ui| {
|
|
||||||
for (tt, tt_name) in [
|
|
||||||
(TokenType::Comment, "// comment"),
|
|
||||||
(TokenType::Keyword, "keyword"),
|
|
||||||
(TokenType::Literal, "literal"),
|
|
||||||
(TokenType::StringLiteral, "\"string literal\""),
|
|
||||||
(TokenType::Punctuation, "punctuation ;"),
|
|
||||||
// (TokenType::Whitespace, "whitespace"),
|
|
||||||
] {
|
|
||||||
let format = &mut self.formats[tt];
|
|
||||||
ui.style_mut().override_font_id = Some(format.font_id.clone());
|
|
||||||
ui.visuals_mut().override_text_color = Some(format.color);
|
|
||||||
ui.radio_value(&mut selected_tt, tt, tt_name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let reset_value = if self.dark_mode {
|
|
||||||
CodeTheme::dark()
|
|
||||||
} else {
|
|
||||||
CodeTheme::light()
|
|
||||||
};
|
|
||||||
|
|
||||||
if ui
|
|
||||||
.add_enabled(*self != reset_value, egui::Button::new("Reset theme"))
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
*self = reset_value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(16.0);
|
|
||||||
|
|
||||||
ui.data_mut(|d| d.insert_persisted(selected_id, selected_tt));
|
|
||||||
|
|
||||||
egui::Frame::group(ui.style())
|
|
||||||
.inner_margin(egui::Vec2::splat(2.0))
|
|
||||||
.show(ui, |ui| {
|
|
||||||
// ui.group(|ui| {
|
|
||||||
ui.style_mut().override_text_style = Some(egui::TextStyle::Small);
|
|
||||||
ui.spacing_mut().slider_width = 128.0; // Controls color picker size
|
|
||||||
egui::widgets::color_picker::color_picker_color32(
|
|
||||||
ui,
|
|
||||||
&mut self.formats[selected_tt].color,
|
|
||||||
egui::color_picker::Alpha::Opaque,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[cfg(feature = "syntect")]
|
|
||||||
struct Highlighter {
|
|
||||||
ps: syntect::parsing::SyntaxSet,
|
|
||||||
ts: syntect::highlighting::ThemeSet,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "syntect")]
|
|
||||||
impl Default for Highlighter {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
ps: syntect::parsing::SyntaxSet::load_defaults_newlines(),
|
|
||||||
ts: syntect::highlighting::ThemeSet::load_defaults(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "syntect")]
|
|
||||||
impl Highlighter {
|
|
||||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
|
||||||
fn highlight(&self, theme: &CodeTheme, code: &str, lang: &str) -> LayoutJob {
|
|
||||||
self.highlight_impl(theme, code, lang).unwrap_or_else(|| {
|
|
||||||
// Fallback:
|
|
||||||
LayoutJob::simple(
|
|
||||||
code.into(),
|
|
||||||
egui::FontId::monospace(12.0),
|
|
||||||
if theme.dark_mode {
|
|
||||||
egui::Color32::LIGHT_GRAY
|
|
||||||
} else {
|
|
||||||
egui::Color32::DARK_GRAY
|
|
||||||
},
|
|
||||||
f32::INFINITY,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn highlight_impl(&self, theme: &CodeTheme, text: &str, language: &str) -> Option<LayoutJob> {
|
|
||||||
use syntect::easy::HighlightLines;
|
|
||||||
use syntect::highlighting::FontStyle;
|
|
||||||
use syntect::util::LinesWithEndings;
|
|
||||||
|
|
||||||
let syntax = self
|
|
||||||
.ps
|
|
||||||
.find_syntax_by_name(language)
|
|
||||||
.or_else(|| self.ps.find_syntax_by_extension(language))?;
|
|
||||||
|
|
||||||
let theme = theme.syntect_theme.syntect_key_name();
|
|
||||||
let mut h = HighlightLines::new(syntax, &self.ts.themes[theme]);
|
|
||||||
|
|
||||||
use egui::text::{LayoutSection, TextFormat};
|
|
||||||
|
|
||||||
let mut job = LayoutJob {
|
|
||||||
text: text.into(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
for line in LinesWithEndings::from(text) {
|
|
||||||
for (style, range) in h.highlight_line(line, &self.ps).ok()? {
|
|
||||||
let fg = style.foreground;
|
|
||||||
let text_color = egui::Color32::from_rgb(fg.r, fg.g, fg.b);
|
|
||||||
let italics = style.font_style.contains(FontStyle::ITALIC);
|
|
||||||
let underline = style.font_style.contains(FontStyle::ITALIC);
|
|
||||||
let underline = if underline {
|
|
||||||
egui::Stroke::new(1.0, text_color)
|
|
||||||
} else {
|
|
||||||
egui::Stroke::NONE
|
|
||||||
};
|
|
||||||
job.sections.push(LayoutSection {
|
|
||||||
leading_space: 0.0,
|
|
||||||
byte_range: as_byte_range(text, range),
|
|
||||||
format: TextFormat {
|
|
||||||
font_id: egui::FontId::monospace(12.0),
|
|
||||||
color: text_color,
|
|
||||||
italics,
|
|
||||||
underline,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(job)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "syntect")]
|
|
||||||
fn as_byte_range(whole: &str, range: &str) -> std::ops::Range<usize> {
|
|
||||||
let whole_start = whole.as_ptr() as usize;
|
|
||||||
let range_start = range.as_ptr() as usize;
|
|
||||||
assert!(whole_start <= range_start);
|
|
||||||
assert!(range_start + range.len() <= whole_start + whole.len());
|
|
||||||
let offset = range_start - whole_start;
|
|
||||||
offset..(offset + range.len())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[cfg(not(feature = "syntect"))]
|
|
||||||
#[derive(Default)]
|
|
||||||
struct Highlighter {}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "syntect"))]
|
|
||||||
impl Highlighter {
|
|
||||||
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
|
|
||||||
fn highlight(&self, theme: &CodeTheme, mut text: &str, _language: &str) -> LayoutJob {
|
|
||||||
// Extremely simple syntax highlighter for when we compile without syntect
|
|
||||||
|
|
||||||
let mut job = LayoutJob::default();
|
|
||||||
|
|
||||||
while !text.is_empty() {
|
|
||||||
if text.starts_with("//") {
|
|
||||||
let end = text.find('\n').unwrap_or(text.len());
|
|
||||||
job.append(&text[..end], 0.0, theme.formats[TokenType::Comment].clone());
|
|
||||||
text = &text[end..];
|
|
||||||
} else if text.starts_with('"') {
|
|
||||||
let end = text[1..]
|
|
||||||
.find('"')
|
|
||||||
.map(|i| i + 2)
|
|
||||||
.or_else(|| text.find('\n'))
|
|
||||||
.unwrap_or(text.len());
|
|
||||||
job.append(
|
|
||||||
&text[..end],
|
|
||||||
0.0,
|
|
||||||
theme.formats[TokenType::StringLiteral].clone(),
|
|
||||||
);
|
|
||||||
text = &text[end..];
|
|
||||||
} else if text.starts_with(|c: char| c.is_ascii_alphanumeric()) {
|
|
||||||
let end = text[1..]
|
|
||||||
.find(|c: char| !c.is_ascii_alphanumeric())
|
|
||||||
.map_or_else(|| text.len(), |i| i + 1);
|
|
||||||
let word = &text[..end];
|
|
||||||
let tt = if is_keyword(word) {
|
|
||||||
TokenType::Keyword
|
|
||||||
} else {
|
|
||||||
TokenType::Literal
|
|
||||||
};
|
|
||||||
job.append(word, 0.0, theme.formats[tt].clone());
|
|
||||||
text = &text[end..];
|
|
||||||
} else if text.starts_with(|c: char| c.is_ascii_whitespace()) {
|
|
||||||
let end = text[1..]
|
|
||||||
.find(|c: char| !c.is_ascii_whitespace())
|
|
||||||
.map_or_else(|| text.len(), |i| i + 1);
|
|
||||||
job.append(
|
|
||||||
&text[..end],
|
|
||||||
0.0,
|
|
||||||
theme.formats[TokenType::Whitespace].clone(),
|
|
||||||
);
|
|
||||||
text = &text[end..];
|
|
||||||
} else {
|
|
||||||
let mut it = text.char_indices();
|
|
||||||
it.next();
|
|
||||||
let end = it.next().map_or(text.len(), |(idx, _chr)| idx);
|
|
||||||
job.append(
|
|
||||||
&text[..end],
|
|
||||||
0.0,
|
|
||||||
theme.formats[TokenType::Punctuation].clone(),
|
|
||||||
);
|
|
||||||
text = &text[end..];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
job
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "syntect"))]
|
|
||||||
fn is_keyword(word: &str) -> bool {
|
|
||||||
matches!(
|
|
||||||
word,
|
|
||||||
"as" | "async"
|
|
||||||
| "await"
|
|
||||||
| "break"
|
|
||||||
| "const"
|
|
||||||
| "continue"
|
|
||||||
| "crate"
|
|
||||||
| "dyn"
|
|
||||||
| "else"
|
|
||||||
| "enum"
|
|
||||||
| "extern"
|
|
||||||
| "false"
|
|
||||||
| "fn"
|
|
||||||
| "for"
|
|
||||||
| "if"
|
|
||||||
| "impl"
|
|
||||||
| "in"
|
|
||||||
| "let"
|
|
||||||
| "loop"
|
|
||||||
| "match"
|
|
||||||
| "mod"
|
|
||||||
| "move"
|
|
||||||
| "mut"
|
|
||||||
| "pub"
|
|
||||||
| "ref"
|
|
||||||
| "return"
|
|
||||||
| "self"
|
|
||||||
| "Self"
|
|
||||||
| "static"
|
|
||||||
| "struct"
|
|
||||||
| "super"
|
|
||||||
| "trait"
|
|
||||||
| "true"
|
|
||||||
| "type"
|
|
||||||
| "unsafe"
|
|
||||||
| "use"
|
|
||||||
| "where"
|
|
||||||
| "while"
|
|
||||||
)
|
|
||||||
}
|
|
133
tools/packet_inspector/src/tri_checkbox.rs
Normal file
133
tools/packet_inspector/src/tri_checkbox.rs
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
use egui::{
|
||||||
|
epaint, pos2, vec2, NumExt, Response, Sense, Shape, TextStyle, Ui, Vec2, Widget, WidgetInfo,
|
||||||
|
WidgetText, WidgetType,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum TriCheckboxState {
|
||||||
|
Enabled,
|
||||||
|
Partial,
|
||||||
|
Disabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(emilk): allow checkbox without a text label
|
||||||
|
/// Boolean on/off control with text label.
|
||||||
|
///
|
||||||
|
/// Usually you'd use [`Ui::checkbox`] instead.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # egui::__run_test_ui(|ui| {
|
||||||
|
/// # let mut my_bool = true;
|
||||||
|
/// // These are equivalent:
|
||||||
|
/// ui.checkbox(&mut my_bool, "Checked");
|
||||||
|
/// ui.add(egui::Checkbox::new(&mut my_bool, "Checked"));
|
||||||
|
/// # });
|
||||||
|
/// ```
|
||||||
|
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
||||||
|
pub struct TriCheckbox<'a> {
|
||||||
|
checked: &'a mut TriCheckboxState,
|
||||||
|
text: WidgetText,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
impl<'a> TriCheckbox<'a> {
|
||||||
|
pub fn new(checked: &'a mut TriCheckboxState, text: impl Into<WidgetText>) -> Self {
|
||||||
|
TriCheckbox {
|
||||||
|
checked,
|
||||||
|
text: text.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn without_text(checked: &'a mut TriCheckboxState) -> Self {
|
||||||
|
Self::new(checked, WidgetText::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget for TriCheckbox<'a> {
|
||||||
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
|
let TriCheckbox { checked, text } = self;
|
||||||
|
|
||||||
|
let spacing = &ui.spacing();
|
||||||
|
let icon_width = spacing.icon_width;
|
||||||
|
let icon_spacing = spacing.icon_spacing;
|
||||||
|
|
||||||
|
let (text, mut desired_size) = if text.is_empty() {
|
||||||
|
(None, vec2(icon_width, 0.0))
|
||||||
|
} else {
|
||||||
|
let total_extra = vec2(icon_width + icon_spacing, 0.0);
|
||||||
|
|
||||||
|
let wrap_width = ui.available_width() - total_extra.x;
|
||||||
|
let text = text.into_galley(ui, None, wrap_width, TextStyle::Button);
|
||||||
|
|
||||||
|
let mut desired_size = total_extra + text.size();
|
||||||
|
desired_size = desired_size.at_least(spacing.interact_size);
|
||||||
|
|
||||||
|
(Some(text), desired_size)
|
||||||
|
};
|
||||||
|
|
||||||
|
desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y));
|
||||||
|
desired_size.y = desired_size.y.max(icon_width);
|
||||||
|
let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
|
||||||
|
|
||||||
|
if response.clicked() {
|
||||||
|
*checked = match *checked {
|
||||||
|
TriCheckboxState::Partial | TriCheckboxState::Disabled => TriCheckboxState::Enabled,
|
||||||
|
TriCheckboxState::Enabled => TriCheckboxState::Disabled,
|
||||||
|
};
|
||||||
|
response.mark_changed();
|
||||||
|
}
|
||||||
|
response.widget_info(|| {
|
||||||
|
WidgetInfo::selected(
|
||||||
|
WidgetType::Checkbox,
|
||||||
|
*checked == TriCheckboxState::Enabled,
|
||||||
|
text.as_ref().map_or("", |x| x.text()),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
if ui.is_rect_visible(rect) {
|
||||||
|
// let visuals = ui.style().interact_selectable(&response, *checked); // too colorful
|
||||||
|
let visuals = ui.style().interact(&response);
|
||||||
|
let (small_icon_rect, big_icon_rect) = ui.spacing().icon_rectangles(rect);
|
||||||
|
ui.painter().add(epaint::RectShape {
|
||||||
|
rect: big_icon_rect.expand(visuals.expansion),
|
||||||
|
rounding: visuals.rounding,
|
||||||
|
fill: visuals.bg_fill,
|
||||||
|
stroke: visuals.bg_stroke,
|
||||||
|
});
|
||||||
|
|
||||||
|
match *checked {
|
||||||
|
TriCheckboxState::Enabled => {
|
||||||
|
// Check mark:
|
||||||
|
ui.painter().add(Shape::line(
|
||||||
|
vec![
|
||||||
|
pos2(small_icon_rect.left(), small_icon_rect.center().y),
|
||||||
|
pos2(small_icon_rect.center().x, small_icon_rect.bottom()),
|
||||||
|
pos2(small_icon_rect.right(), small_icon_rect.top()),
|
||||||
|
],
|
||||||
|
visuals.fg_stroke,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
TriCheckboxState::Partial => {
|
||||||
|
// Minus sign:
|
||||||
|
ui.painter().add(Shape::line(
|
||||||
|
vec![
|
||||||
|
pos2(small_icon_rect.left(), small_icon_rect.center().y),
|
||||||
|
pos2(small_icon_rect.right(), small_icon_rect.center().y),
|
||||||
|
],
|
||||||
|
visuals.fg_stroke,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
if let Some(text) = text {
|
||||||
|
let text_pos = pos2(
|
||||||
|
rect.min.x + icon_width + icon_spacing,
|
||||||
|
rect.center().y - 0.5 * text.size().y,
|
||||||
|
);
|
||||||
|
text.paint_with_visuals(ui.painter(), text_pos, visuals);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue