# encoding: UTF-8 require "zlib" require_relative "test_helper" require_relative "../lib/meow/telnet" module MeowTelnetTest Telnet = MeowClient::Telnet def self.bytes(*values) values.pack("C*") end def self.collect(parser, chunks) chunks.each_with_object({:data => +"".b, :replies => [], :events => []}) do |chunk, result| parsed = parser.feed(chunk) result[:data] << parsed.data result[:replies].concat(parsed.replies) result[:events].concat(parsed.events) end end MeowTest.test("Telnet preserves data and unescapes IAC") do parser = Telnet::Parser.new result = parser.feed("look".b + bytes(Telnet::IAC, Telnet::IAC) + "!".b) MeowTest.assert_equal("look".b + bytes(Telnet::IAC) + "!".b, result.data) MeowTest.assert_empty(result.replies) MeowTest.assert_empty(result.events) end MeowTest.test("Telnet negotiation survives every input boundary") do packet = bytes(Telnet::IAC, Telnet::WILL, Telnet::ECHO) whole = collect(Telnet::Parser.new, [packet]) split = collect(Telnet::Parser.new, packet.bytes.map { |byte| bytes(byte) }) MeowTest.assert_equal(whole, split) MeowTest.assert_equal([bytes(Telnet::IAC, Telnet::DO, Telnet::ECHO)], split[:replies]) MeowTest.assert_equal([[:remote_echo, true]], split[:events]) end MeowTest.test("Telnet tracks echo transitions without duplicate replies") do parser = Telnet::Parser.new enabled = parser.feed(bytes(Telnet::IAC, Telnet::WILL, Telnet::ECHO)) duplicate = parser.feed(bytes(Telnet::IAC, Telnet::WILL, Telnet::ECHO)) disabled = parser.feed(bytes(Telnet::IAC, Telnet::WONT, Telnet::ECHO)) disabled_duplicate = parser.feed(bytes(Telnet::IAC, Telnet::WONT, Telnet::ECHO)) MeowTest.assert_equal([bytes(Telnet::IAC, Telnet::DO, Telnet::ECHO)], enabled.replies) MeowTest.assert_empty(duplicate.replies) MeowTest.assert_equal([bytes(Telnet::IAC, Telnet::DONT, Telnet::ECHO)], disabled.replies) MeowTest.assert_equal([[:remote_echo, false]], disabled.events) MeowTest.assert_empty(disabled_duplicate.replies) MeowTest.refute(parser.remote_echo) end MeowTest.test("Telnet accepts supported options and rejects unknown options") do parser = Telnet::Parser.new remote = parser.feed(bytes(Telnet::IAC, Telnet::WILL, Telnet::SUPPRESS_GO_AHEAD)) remote_unknown = parser.feed(bytes(Telnet::IAC, Telnet::WILL, 42)) local = parser.feed(bytes(Telnet::IAC, Telnet::DO, Telnet::TERMINAL_TYPE)) local_unknown = parser.feed(bytes(Telnet::IAC, Telnet::DO, 43)) MeowTest.assert_equal([bytes(Telnet::IAC, Telnet::DO, Telnet::SUPPRESS_GO_AHEAD)], remote.replies) MeowTest.assert_equal([bytes(Telnet::IAC, Telnet::DONT, 42)], remote_unknown.replies) MeowTest.assert_equal([bytes(Telnet::IAC, Telnet::WILL, Telnet::TERMINAL_TYPE)], local.replies) MeowTest.assert_equal([bytes(Telnet::IAC, Telnet::WONT, 43)], local_unknown.replies) end MeowTest.test("Telnet sends terminal type only after accepting the option") do request = bytes(Telnet::IAC, Telnet::SB, Telnet::TERMINAL_TYPE, Telnet::TTYPE_SEND, Telnet::IAC, Telnet::SE) parser = Telnet::Parser.new before = parser.feed(request) parser.feed(bytes(Telnet::IAC, Telnet::DO, Telnet::TERMINAL_TYPE)) after = collect(parser, request.bytes.map { |byte| bytes(byte) }) expected = bytes(Telnet::IAC, Telnet::SB, Telnet::TERMINAL_TYPE, Telnet::TTYPE_IS) + "MEOW".b + bytes(Telnet::IAC, Telnet::SE) MeowTest.assert_empty(before.replies) MeowTest.assert_equal([[:subnegotiation, Telnet::TERMINAL_TYPE]], before.events) MeowTest.assert_equal([expected], after[:replies]) MeowTest.assert_equal([[:terminal_type, "MEOW"]], after[:events]) end MeowTest.test("Telnet handles escaped IAC inside subnegotiation") do parser = Telnet::Parser.new packet = bytes(Telnet::IAC, Telnet::SB, 77, 1, Telnet::IAC, Telnet::IAC, 2, Telnet::IAC, Telnet::SE) result = collect(parser, packet.bytes.map { |byte| bytes(byte) }) MeowTest.assert_empty(result[:data]) MeowTest.assert_empty(result[:replies]) MeowTest.assert_equal([[:subnegotiation, 77]], result[:events]) end MeowTest.test("Telnet ignores unknown commands and resumes ordinary data") do parser = Telnet::Parser.new result = parser.feed("a".b + bytes(Telnet::IAC, 241) + "b".b) MeowTest.assert_equal("ab".b, result.data) 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