Add MSP media protocol support

This commit is contained in:
2026-09-17 17:53:08 +02:00
parent 3fd28c2e0b
commit 573f1b7acb
14 changed files with 357 additions and 51 deletions
+25 -4
View File
@@ -14,8 +14,8 @@ module MeowMediaTest
@requests = []
end
def enqueue(id, uri)
@requests << [id, uri]
def enqueue(id, uri, allow_insecure_http: false)
@requests << [id, uri, allow_insecure_http]
true
end
@@ -76,13 +76,34 @@ module MeowMediaTest
MeowTest.assert_equal("https://media.example/game/weather/rain.ogg", uri.to_s)
end
MeowTest.test("Media URL policy rejects insecure private and traversing URLs") do
MeowTest.test("Media URL policy isolates legacy HTTP from secure requests") do
policy = Media::URLPolicy.new
MeowTest.assert_raises(ArgumentError) { policy.resolve("http://example.com/", "sound.mp3") }
MeowTest.assert_raises(ArgumentError) { policy.resolve("https://127.0.0.1/", "sound.mp3") }
legacy = policy.resolve("http://example.com/", "sound.mp3", nil, :allow_insecure_http => true)
MeowTest.assert_equal("http://example.com/sound.mp3", legacy.to_s)
MeowTest.assert_raises(ArgumentError) do
policy.resolve("http://127.0.0.1/", "sound.mp3", nil, :allow_insecure_http => true)
end
MeowTest.assert_raises(ArgumentError) { policy.resolve("https://example.com/", "../secret.mp3") }
end
MeowTest.test("Media manager carries HTTP permission on each request") do
downloader = FakeDownloader.new
manager = Media::Manager.new("unused", :downloader => downloader)
secure = Media::PlayRequest.new(:name => "secure.mp3", :url => "https://media.example/")
legacy = Media::PlayRequest.new(
:name => "legacy.mid",
:url => "http://media.example/",
:allow_insecure_http => true
)
manager.play(secure)
manager.play(legacy)
MeowTest.assert_equal(false, downloader.requests[0][2])
MeowTest.assert_equal(true, downloader.requests[1][2])
ensure
manager.close if manager != nil
end
MeowTest.test("Client.Media adapter normalizes play and stop commands") do
play = Media::ClientMedia.play_request({"name" => "rain.ogg", "type" => "music", "loops" => -1, "caption" => "Rain"})
stop = Media::ClientMedia.stop_request({"type" => "music", "fadeaway" => true})
+68
View File
@@ -0,0 +1,68 @@
# encoding: UTF-8
require_relative "test_helper"
require_relative "../lib/meow/media"
require_relative "../lib/meow/msp"
module MeowMSPTest
MSP = MeowClient::MSP
MeowTest.test("MSP maps sound defaults into a shared media request") do
action = MSP::Adapter.new.parse("!!SOUND(weather/thunder)")
request = action.request
MeowTest.assert_equal(:play, action.type)
MeowTest.assert_equal("weather/thunder.wav", request.name)
MeowTest.assert_equal("sound", request.kind)
MeowTest.assert_equal("weather", request.tag)
MeowTest.assert_equal(100, request.volume)
MeowTest.assert_equal(1, request.loops)
MeowTest.assert_equal(50, request.priority)
MeowTest.refute(request.continue_existing)
MeowTest.assert(request.allow_insecure_http)
end
MeowTest.test("MSP accepts case-insensitive parameters and quoted legacy URLs") do
action = MSP::Adapter.new.parse(
'!!MUSIC(theme v=75 l=-1 c=0 t=zone u="http://media.example/game sounds/")'
)
request = action.request
MeowTest.assert_equal("theme.mid", request.name)
MeowTest.assert_equal("music", request.kind)
MeowTest.assert_equal("zone", request.tag)
MeowTest.assert_equal(75, request.volume)
MeowTest.assert_equal(-1, request.loops)
MeowTest.refute(request.continue_existing)
MeowTest.assert_equal("msp:music", request.key)
MeowTest.assert_equal("http://media.example/game sounds/", request.url)
end
MeowTest.test("MSP default URL and stop actions remain protocol-local") do
adapter = MSP::Adapter.new
default_action = adapter.parse("!!SOUND(Off U=http://media.example/sounds/)")
MeowTest.assert_equal(:default_url, default_action.type)
adapter.default_url = default_action.url
play = adapter.parse("!!SOUND(bell.wav)")
stop = adapter.parse("!!MUSIC(Off)")
MeowTest.assert_equal("http://media.example/sounds/", play.request.url)
MeowTest.assert_equal(:stop, stop.type)
MeowTest.assert_equal("music", stop.request.kind)
end
MeowTest.test("MSP recognizes out-of-band bodies but not mid-line text") do
adapter = MSP::Adapter.new
action = adapter.parse_bytes("SOUND(chime.wav U=http://media.example/)".b, "UTF-8")
MeowTest.assert_equal(:play, action.type)
MeowTest.assert_equal(nil, adapter.parse("chat says !!SOUND(chime.wav)"))
end
MeowTest.test("MSP reports invalid values and unsupported wildcards") do
adapter = MSP::Adapter.new
bad_volume = adapter.parse("!!SOUND(chime.wav V=101)")
wildcard = adapter.parse("!!SOUND(chime*.wav)")
unterminated = adapter.parse('!!SOUND(chime.wav U="http://media.example/)')
MeowTest.assert_equal(:error, bad_volume.type)
MeowTest.assert_equal(:error, wildcard.type)
MeowTest.assert_equal(:error, unterminated.type)
end
end
+1
View File
@@ -2,6 +2,7 @@ require_relative "test_helper"
require_relative "telnet_test"
require_relative "gmcp_test"
require_relative "media_test"
require_relative "msp_test"
require_relative "text_pipeline_test"
begin
+18
View File
@@ -66,6 +66,24 @@ module MeowTelnetTest
MeowTest.assert_equal([bytes(Telnet::IAC, Telnet::WONT, 43)], local_unknown.replies)
end
MeowTest.test("Telnet negotiates MSP only when media is enabled") do
disabled = Telnet::Parser.new.feed(bytes(Telnet::IAC, Telnet::WILL, Telnet::MSP))
enabled = Telnet::Parser.new(:enable_msp => true).feed(bytes(Telnet::IAC, Telnet::WILL, Telnet::MSP))
MeowTest.assert_equal([bytes(Telnet::IAC, Telnet::DONT, Telnet::MSP)], disabled.replies)
MeowTest.assert_empty(disabled.events)
MeowTest.assert_equal([bytes(Telnet::IAC, Telnet::DO, Telnet::MSP)], enabled.replies)
MeowTest.assert_equal([[:msp_enabled, true]], enabled.events)
end
MeowTest.test("Telnet emits negotiated MSP subnegotiation payloads") do
parser = Telnet::Parser.new(:enable_msp => true)
parser.feed(bytes(Telnet::IAC, Telnet::WILL, Telnet::MSP))
body = "SOUND(chime.wav U=http://media.example/)".b
packet = bytes(Telnet::IAC, Telnet::SB, Telnet::MSP) + body + bytes(Telnet::IAC, Telnet::SE)
result = collect(parser, packet.bytes.map { |byte| bytes(byte) })
MeowTest.assert_equal([[:msp, body]], result[:events])
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