Add MCCP2 compressed stream support
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -94,6 +94,10 @@ module MeowClient
|
||||
prompt == nil ? [] : [prompt]
|
||||
end
|
||||
|
||||
def close
|
||||
@telnet.close
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def decode(bytes)
|
||||
|
||||
Reference in New Issue
Block a user