Add MCCP2 compressed stream support

This commit is contained in:
2026-09-17 16:18:16 +02:00
parent da967f4d56
commit 158ee2568c
8 changed files with 238 additions and 45 deletions
+102
View File
@@ -1,5 +1,7 @@
# encoding: UTF-8
require "zlib"
require_relative "test_helper"
require_relative "../lib/meow/telnet"
@@ -94,4 +96,104 @@ module MeowTelnetTest
MeowTest.assert_empty(result.replies)
MeowTest.assert_empty(result.events)
end
MeowTest.test("Telnet negotiates MCCP2 once") do
parser = Telnet::Parser.new
packet = bytes(Telnet::IAC, Telnet::WILL, Telnet::COMPRESS2)
first = parser.feed(packet)
duplicate = parser.feed(packet)
MeowTest.assert_equal([bytes(Telnet::IAC, Telnet::DO, Telnet::COMPRESS2)], first.replies)
MeowTest.assert_empty(duplicate.replies)
end
MeowTest.test("Telnet starts MCCP2 only after negotiation") do
marker = bytes(Telnet::IAC, Telnet::SB, Telnet::COMPRESS2, Telnet::IAC, Telnet::SE)
parser = Telnet::Parser.new
before = parser.feed(marker)
parser.feed(bytes(Telnet::IAC, Telnet::WILL, Telnet::COMPRESS2))
after = parser.feed(marker + Zlib::Deflate.deflate("compressed text".b))
MeowTest.assert_equal([[:subnegotiation, Telnet::COMPRESS2]], before.events)
MeowTest.assert_equal("compressed text".b, after.data)
MeowTest.assert_equal([[:compression, true]], after.events)
ensure
parser.close if parser != nil
end
MeowTest.test("MCCP2 survives every activation and compressed input boundary") do
negotiation = bytes(Telnet::IAC, Telnet::WILL, Telnet::COMPRESS2)
marker = bytes(Telnet::IAC, Telnet::SB, Telnet::COMPRESS2, Telnet::IAC, Telnet::SE)
payload = "room description\r\n".b
packet = negotiation + marker + Zlib::Deflate.deflate(payload)
whole_parser = Telnet::Parser.new
split_parser = Telnet::Parser.new
whole = collect(whole_parser, [packet])
split = collect(split_parser, packet.bytes.map { |byte| bytes(byte) })
MeowTest.assert_equal(whole, split)
MeowTest.assert_equal(payload, split[:data])
MeowTest.assert_equal([[:compression, true]], split[:events])
ensure
whole_parser.close if whole_parser != nil
split_parser.close if split_parser != nil
end
MeowTest.test("MCCP2 inflated bytes continue through Telnet parsing") do
parser = Telnet::Parser.new
parser.feed(bytes(Telnet::IAC, Telnet::WILL, Telnet::COMPRESS2))
marker = bytes(Telnet::IAC, Telnet::SB, Telnet::COMPRESS2, Telnet::IAC, Telnet::SE)
payload = "hello".b + bytes(Telnet::IAC, Telnet::WILL, Telnet::ECHO) + " world".b
result = parser.feed(marker + Zlib::Deflate.deflate(payload))
MeowTest.assert_equal("hello world".b, result.data)
MeowTest.assert_equal([bytes(Telnet::IAC, Telnet::DO, Telnet::ECHO)], result.replies)
MeowTest.assert_equal([[:compression, true], [:remote_echo, true]], result.events)
ensure
parser.close if parser != nil
end
MeowTest.test("MCCP2 keeps one compressed stream across input chunks") do
parser = Telnet::Parser.new
deflater = Zlib::Deflate.new
parser.feed(bytes(Telnet::IAC, Telnet::WILL, Telnet::COMPRESS2))
marker = bytes(Telnet::IAC, Telnet::SB, Telnet::COMPRESS2, Telnet::IAC, Telnet::SE)
first = deflater.deflate("first ".b, Zlib::SYNC_FLUSH)
MeowTest.assert_equal("first ".b, parser.feed(marker + first).data)
second = deflater.deflate("second".b, Zlib::SYNC_FLUSH)
MeowTest.assert_equal("second".b, parser.feed(second).data)
ensure
parser.close if parser != nil
deflater.close if deflater != nil && !deflater.closed?
end
MeowTest.test("MCCP2 rejects malformed streams and data after stream end") do
marker = bytes(Telnet::IAC, Telnet::SB, Telnet::COMPRESS2, Telnet::IAC, Telnet::SE)
malformed = Telnet::Parser.new
malformed.feed(bytes(Telnet::IAC, Telnet::WILL, Telnet::COMPRESS2))
MeowTest.assert_raises(Telnet::CompressionError) { malformed.feed(marker + "not zlib".b) }
finished = Telnet::Parser.new
finished.feed(bytes(Telnet::IAC, Telnet::WILL, Telnet::COMPRESS2))
finished.feed(marker + Zlib::Deflate.deflate("done".b))
MeowTest.assert_raises(Telnet::CompressionError) { finished.feed("extra".b) }
ensure
malformed.close if malformed != nil
finished.close if finished != nil
end
MeowTest.test("MCCP2 limits inflated output per feed") do
parser = Telnet::Parser.new
parser.feed(bytes(Telnet::IAC, Telnet::WILL, Telnet::COMPRESS2))
marker = bytes(Telnet::IAC, Telnet::SB, Telnet::COMPRESS2, Telnet::IAC, Telnet::SE)
oversized = "a".b * (Telnet::MAX_INFLATED_BYTES_PER_FEED + 1)
MeowTest.assert_raises(Telnet::CompressionError) do
parser.feed(marker + Zlib::Deflate.deflate(oversized))
end
ensure
parser.close if parser != nil
end
end
+10
View File
@@ -29,6 +29,16 @@ module MeowTest
assert(value.empty?, message || "Expected #{value.inspect} to be empty")
end
def assert_raises(error_class)
yield
rescue error_class => error
return error
rescue StandardError => error
raise Failure, "Expected #{error_class}, got #{error.class}: #{error.message}"
else
raise Failure, "Expected #{error_class} to be raised"
end
def run!
failures = []
@tests.each do |name, test|
+16
View File
@@ -1,5 +1,7 @@
# encoding: UTF-8
require "zlib"
require_relative "test_helper"
require_relative "../lib/meow/telnet"
require_relative "../lib/meow/text_pipeline"
@@ -74,4 +76,18 @@ module MeowTextPipelineTest
MeowTest.assert_equal([[:remote_echo, true]], result.events)
MeowTest.assert(pipeline.remote_echo?)
end
MeowTest.test("Text pipeline processes MCCP2 output normally") do
pipeline = MeowClient::TextPipeline.new("UTF-8")
negotiation = bytes(Telnet::IAC, Telnet::WILL, Telnet::COMPRESS2)
marker = bytes(Telnet::IAC, Telnet::SB, Telnet::COMPRESS2, Telnet::IAC, Telnet::SE)
payload = "red\e[31m text\e[0m\r\n".b
result = pipeline.feed(negotiation + marker + Zlib::Deflate.deflate(payload))
MeowTest.assert_equal(["red text"], result.records)
MeowTest.assert_equal([bytes(Telnet::IAC, Telnet::DO, Telnet::COMPRESS2)], result.replies)
MeowTest.assert_equal([[:compression, true]], result.events)
ensure
pipeline.close if pipeline != nil
end
end