Sign in Sign up
kretrod/lodestone Public
Branches
master
203 lines (188 loc) · 7.8 KB Raw
//! Reading the wire.
//!
//! Minecraft's networking is netty, and its pipeline is a named chain of
//! handlers: `decrypt`, `decompress`, `splitter`, `decoder`, then finally
//! `packet_handler`, which is the game itself. Anything spliced in immediately
//! before `packet_handler` therefore sees packets that have already been
//! decrypted, decompressed and decoded — objects, not ciphertext.
//!
//! The obstacle is that a netty handler has to be a Java object, and there is
//! no Java here to write one. JNI's `DefineClass` solves it: a handler compiled
//! ahead of time is handed as bytecode to the class loader that already holds
//! the game's classes, so it can see netty and netty can see it. No mod, no
//! launch flag, no agent — the game is running before any of this exists.
//!
//! The handler only counts and names what passes through. Every message is
//! forwarded untouched, so the game behaves exactly as it would without it.

use jni_sys::{jclass, jfieldID, jmethodID, jobject, jvalue};

use crate::jni::Jni;
use crate::mc::Mc;

/// Compiled from `tap/src/lodestone/Tap.java` against stub netty interfaces —
/// only the signatures are needed to compile, and the real netty is what it
/// links against once it is inside.
const TAP_CLASS: &[u8] = include_bytes!("../assets/Tap.class");

pub struct Tap {
    class: jclass,
    handler: Option<jobject>,
    m_drain: Option<jmethodID>,
    m_summary: Option<jmethodID>,
    m_reset: Option<jmethodID>,
    f_recording: Option<jfieldID>,

    f_listener_connection: Option<jfieldID>,
    f_channel: Option<jfieldID>,
    m_pipeline: Option<jmethodID>,
    m_add_before: Option<jmethodID>,
    m_get_handler: Option<jmethodID>,
    m_tap_init: Option<jmethodID>,
    f_player_connection: Option<jfieldID>,
}

impl Tap {
    pub fn resolve(j: &Jni, mc: &Mc) -> Option<Tap> {
        // Define into the loader that owns the game's classes, so netty
        // resolves to the same netty the game is using.
        let loader = {
            let class_cls = j.find_class("java/lang/Class")?;
            let m = j.method(class_cls, "getClassLoader", "()Ljava/lang/ClassLoader;")?;
            j.call_obj(mc.minecraft as jobject, m, &[])?
        };
        // A previous resident copy of the client may already have defined this
        // class into the loader, and a loader refuses a second definition of
        // the same name. So define it, and if that is refused, find the one
        // that is already there.
        let defined = j.define_class("lodestone/Tap", loader, TAP_CLASS);
        let local = match defined {
            Some(c) => c,
            None => {
                crate::log("tap: class already defined, reusing it");
                j.find_class("lodestone/Tap")?
            }
        };
        let class = j.global(local)? as jclass;
        j.delete_local(local);

        let channel = j.find_class("io/netty/channel/Channel");
        let pipeline = j.find_class("io/netty/channel/ChannelPipeline");
        let connection = j.find_class("net/minecraft/network/Connection");
        let listener = j.find_class("net/minecraft/client/multiplayer/ClientCommonPacketListenerImpl");

        Some(Tap {
            m_drain: j.static_method(class, "drain", "()Ljava/lang/String;"),
            m_summary: j.static_method(class, "summary", "()Ljava/lang/String;"),
            m_reset: j.static_method(class, "reset", "()V"),
            f_recording: j.static_field(class, "recording", "Z"),
            m_tap_init: j.method(class, "<init>", "()V"),
            class,
            handler: None,

            f_listener_connection: listener.and_then(|c| {
                j.field(c, "connection", "Lnet/minecraft/network/Connection;")
            }),
            f_channel: connection
                .and_then(|c| j.field(c, "channel", "Lio/netty/channel/Channel;")),
            m_pipeline: channel
                .and_then(|c| j.method(c, "pipeline", "()Lio/netty/channel/ChannelPipeline;")),
            m_add_before: pipeline.and_then(|c| {
                j.method(
                    c,
                    "addBefore",
                    "(Ljava/lang/String;Ljava/lang/String;Lio/netty/channel/ChannelHandler;)Lio/netty/channel/ChannelPipeline;",
                )
            }),
            m_get_handler: pipeline.and_then(|c| {
                j.method(c, "get", "(Ljava/lang/String;)Lio/netty/channel/ChannelHandler;")
            }),
            f_player_connection: j.field(
                mc.local_player,
                "connection",
                "Lnet/minecraft/client/multiplayer/ClientPacketListener;",
            ),
        })
    }

    /// Splice the handler in, once per connection. Re-checked because joining
    /// a different server builds a new pipeline and loses the old one.
    pub fn attach(&mut self, j: &Jni, player: jobject) -> bool {
        let Some(pipeline) = self.pipeline(j, player) else {
            return false;
        };
        // Already there? Then this is the same connection as last time.
        if let (Some(get), Some(name)) = (self.m_get_handler, j.new_string("lodestone_tap")) {
            let existing = j.call_obj(pipeline, get, &[jvalue { l: name }]);
            j.delete_local(name);
            if existing.is_some() {
                return true;
            }
        }
        let (Some(add), Some(init)) = (self.m_add_before, self.m_tap_init) else {
            return false;
        };
        let Some(handler) = j.new_object(self.class, init, &[]) else {
            return false;
        };
        let (Some(base), Some(name)) =
            (j.new_string("packet_handler"), j.new_string("lodestone_tap"))
        else {
            return false;
        };
        let ok = j
            .call_obj(
                pipeline,
                add,
                &[
                    jvalue { l: base },
                    jvalue { l: name },
                    jvalue { l: handler },
                ],
            )
            .is_some();
        j.delete_local(base);
        j.delete_local(name);
        if ok {
            self.handler = j.global(handler);
        }
        j.delete_local(handler);
        ok
    }

    fn pipeline(&self, j: &Jni, player: jobject) -> Option<jobject> {
        let listener = j.obj_field(player, self.f_player_connection?)?;
        let connection = j.obj_field(listener, self.f_listener_connection?)?;
        let channel = j.obj_field(connection, self.f_channel?)?;
        j.call_obj(channel, self.m_pipeline?, &[])
    }

    pub fn set_recording(&self, j: &Jni, on: bool) {
        if let Some(f) = self.f_recording {
            // SAFETY-equivalent: a static boolean on our own class.
            j.set_static_bool(self.class, f, on);
        }
    }

    /// Everything seen since the last call.
    pub fn drain(&self, j: &Jni) -> Option<String> {
        let s = j.call_static_obj(self.class, self.m_drain?, &[])?;
        let out = j.rust_string(s);
        j.delete_local(s);
        out
    }

    /// Running totals, as `name sent received` per line.
    pub fn summary(&self, j: &Jni) -> Option<Vec<(String, u32, u32)>> {
        let s = j.call_static_obj(self.class, self.m_summary?, &[])?;
        let text = j.rust_string(s);
        j.delete_local(s);
        let text = text?;
        let mut out = Vec::new();
        for line in text.lines() {
            let mut parts = line.split(' ');
            let (Some(name), Some(sent), Some(recv)) =
                (parts.next(), parts.next(), parts.next())
            else {
                continue;
            };
            out.push((
                name.to_string(),
                sent.parse().unwrap_or(0),
                recv.parse().unwrap_or(0),
            ));
        }
        Some(out)
    }

    pub fn reset(&self, j: &Jni) {
        if let Some(m) = self.m_reset {
            let _ = j.call_static_obj(self.class, m, &[]);
        }
    }
}