azalea_reflection_proxy/plugin.rs
1//! Plugin pipeline — the azalea equivalent of the JS project's plugin
2//! system (anonymize / snapshot / synchronization / inventory / replicator).
3//!
4//! Phase 1 runs zero plugins and just forwards everything; the trait exists
5//! now so phases 2-4 are additive instead of a rewrite. Hooks mirror the
6//! original's: onReadReal ≈ on_clientbound (packets from the target
7//! server), onWriteReal ≈ on_serverbound (packets from the controlling
8//! client), bindToReflected ≈ on_session_start.
9//!
10//! Hooks operate on RAW FRAMES (packet id + payload bytes, post
11//! decryption/decompression) rather than typed packets. This is deliberate:
12//! phase 1 doesn't need to understand packets, and raw frames survive
13//! protocol details the proxy doesn't model. Plugins that need typed access
14//! (snapshot, replicator) parse the frames they care about themselves via
15//! azalea_protocol's read functions and ignore the rest.
16
17/// A raw protocol frame: varint packet id + body, already stripped of
18/// length prefix / compression / encryption.
19#[derive(Clone, Debug)]
20pub struct Frame {
21 pub packet_id: u32,
22 pub body: Vec<u8>,
23}
24
25/// What the pipeline should do with a frame after a plugin saw it.
26pub enum Verdict {
27 /// pass it along unchanged (the overwhelmingly common case)
28 Forward,
29 /// swallow it (e.g. replicator answering a viewer's keepalive locally)
30 Drop,
31 /// substitute different frame(s) (e.g. anonymize rewriting names)
32 Replace(Vec<Frame>),
33}
34
35pub trait ProxyPlugin: Send + Sync {
36 fn name(&self) -> &'static str;
37
38 /// A new session (upstream connection) has been established.
39 fn on_session_start(&self) {}
40
41 /// Frame travelling target-server -> clients.
42 fn on_clientbound(&self, _frame: &Frame) -> Verdict {
43 Verdict::Forward
44 }
45
46 /// Frame travelling controlling-client -> target server.
47 fn on_serverbound(&self, _frame: &Frame) -> Verdict {
48 Verdict::Forward
49 }
50}
51
52/// Runs frames through every plugin in order. First Drop/Replace wins,
53/// matching the original's sequential plugin order semantics.
54pub struct Pipeline {
55 pub plugins: Vec<Box<dyn ProxyPlugin>>,
56}
57
58impl Pipeline {
59 pub fn clientbound(&self, frame: Frame) -> Vec<Frame> {
60 self.route(frame, true)
61 }
62 pub fn serverbound(&self, frame: Frame) -> Vec<Frame> {
63 self.route(frame, false)
64 }
65 fn route(&self, frame: Frame, clientbound: bool) -> Vec<Frame> {
66 for p in &self.plugins {
67 let verdict = if clientbound {
68 p.on_clientbound(&frame)
69 } else {
70 p.on_serverbound(&frame)
71 };
72 match verdict {
73 Verdict::Forward => continue,
74 Verdict::Drop => return Vec::new(),
75 Verdict::Replace(frames) => return frames,
76 }
77 }
78 vec![frame]
79 }
80}