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
+4
View File
@@ -27,6 +27,7 @@ module MeowClient
def run
@form.wait
ensure
@pipeline.close
@transport.close
@program.release(@transport, :close => false)
end
@@ -83,6 +84,9 @@ module MeowClient
receive_record(record) if record != nil
end
dispatch_speech
rescue Telnet::CompressionError
add_system_line(_("The compressed data stream could not be processed safely. The connection was closed."))
close_form
rescue Exception => error
Log.error("Meow session update failed: #{error.class}: #{error.message}") if defined?(Log)
add_system_line(_("Session update error: %{message}") % {:message => error.message})
+95 -38
View File
@@ -1,3 +1,5 @@
require "zlib"
module MeowClient
module Telnet
IAC = 255
@@ -11,10 +13,13 @@ module MeowClient
ECHO = 1
SUPPRESS_GO_AHEAD = 3
TERMINAL_TYPE = 24
COMPRESS2 = 86
TTYPE_IS = 0
TTYPE_SEND = 1
MAX_INFLATED_BYTES_PER_FEED = 1_048_576
Result = Struct.new(:data, :replies, :events, keyword_init: true)
CompressionError = Class.new(StandardError)
class Parser
attr_reader :remote_echo
@@ -27,56 +32,101 @@ module MeowClient
@remote_echo = false
@remote_options = {}
@local_options = {}
@inflater = nil
@compression_finished = false
end
def feed(bytes)
output = +"".b
replies = []
events = []
bytes.to_s.b.each_byte do |byte|
case @state
when :data
if byte == IAC
@state = :iac
else
output << byte
end
when :iac
handle_iac(byte, output)
when :negotiation
reply, event = negotiate(@negotiation_command, byte)
replies << reply if reply != nil
events << event if event != nil
@state = :data
when :sb_option
@subnegotiation_option = byte
@subnegotiation.clear
@state = :sb
when :sb
if byte == IAC
@state = :sb_iac
else
@subnegotiation << byte
end
when :sb_iac
if byte == IAC
@subnegotiation << IAC
@state = :sb
elsif byte == SE
reply, event = finish_subnegotiation
replies << reply if reply != nil
events << event if event != nil
@state = :data
else
@state = :data
end
input = bytes.to_s.b
if @compression_finished && input != ""
raise CompressionError, "Data followed the end of the MCCP2 stream."
end
index = 0
while index < input.bytesize
if @inflater != nil
inflate(input.byteslice(index..-1), output, replies, events)
index = input.bytesize
else
consume_byte(input.getbyte(index), output, replies, events)
index += 1
end
end
Result.new(:data => output, :replies => replies, :events => events)
rescue Zlib::Error => error
raise CompressionError, "Invalid MCCP2 stream: #{error.message}"
end
def close
inflater = @inflater
@inflater = nil
inflater.close if inflater != nil && !inflater.closed?
nil
rescue Zlib::Error
nil
end
private
def consume_byte(byte, output, replies, events)
case @state
when :data
if byte == IAC
@state = :iac
else
output << byte
end
when :iac
handle_iac(byte, output)
when :negotiation
reply, event = negotiate(@negotiation_command, byte)
replies << reply if reply != nil
events << event if event != nil
@state = :data
when :sb_option
@subnegotiation_option = byte
@subnegotiation.clear
@state = :sb
when :sb
if byte == IAC
@state = :sb_iac
else
@subnegotiation << byte
end
when :sb_iac
if byte == IAC
@subnegotiation << IAC
@state = :sb
elsif byte == SE
reply, event = finish_subnegotiation
replies << reply if reply != nil
events << event if event != nil
@state = :data
else
@state = :data
end
end
end
def inflate(bytes, output, replies, events)
inflated_bytes = 0
consume = proc do |chunk|
inflated_bytes += chunk.bytesize
if inflated_bytes > MAX_INFLATED_BYTES_PER_FEED
raise CompressionError, "MCCP2 output exceeded the safety limit."
end
chunk.each_byte { |byte| consume_byte(byte, output, replies, events) }
end
@inflater.inflate(bytes) { |chunk| consume.call(chunk) }
pending = @inflater.flush_next_out
consume.call(pending) if pending != ""
@compression_finished = true if @inflater.finished?
end
def handle_iac(byte, output)
case byte
when IAC
@@ -99,7 +149,7 @@ module MeowClient
def negotiate(command, option)
case command
when WILL
enabled = [ECHO, SUPPRESS_GO_AHEAD].include?(option)
enabled = [ECHO, SUPPRESS_GO_AHEAD, COMPRESS2].include?(option)
changed = @remote_options[option] != enabled
@remote_options[option] = enabled
return [nil, nil] unless changed
@@ -131,6 +181,13 @@ module MeowClient
end
def finish_subnegotiation
if @subnegotiation_option == COMPRESS2 &&
@remote_options[COMPRESS2] == true &&
@subnegotiation.empty?
raise CompressionError, "MCCP2 compression was started more than once." if @inflater != nil
@inflater = Zlib::Inflate.new
return [nil, [:compression, true]]
end
if @subnegotiation_option == TERMINAL_TYPE &&
@local_options[TERMINAL_TYPE] == true &&
@subnegotiation.getbyte(0) == TTYPE_SEND
+4
View File
@@ -94,6 +94,10 @@ module MeowClient
prompt == nil ? [] : [prompt]
end
def close
@telnet.close
end
private
def decode(bytes)