Skip to main content

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}