Skip to content

Commit

Permalink
Merge pull request #1108 from SmartThingsCommunity/fix/sonos-discover…
Browse files Browse the repository at this point in the history
…y-non-ascii-name-failure
  • Loading branch information
dljsjr authored Dec 12, 2023
2 parents a13e994 + 7e4e948 commit c8038a4
Showing 1 changed file with 51 additions and 9 deletions.
60 changes: 51 additions & 9 deletions drivers/SmartThings/sonos/src/ssdp.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,32 @@ SONOS_SSDP_SEARCH_TERM = "urn:smartspeaker-audio:service:SpeakerGroup:1"
local SSDP = {}

local function process_response(val)
local info = {}
-- check first line assuming it's the HTTP Status Line, which if not is invalid
local status_line = string.match(val, "([^\r\n]*)\r\n")
if not (status_line and string.match(status_line, "HTTP/1.1 200 OK")) then
return nil, string.format("SSDP Response HTTP Status Line missing or not '200 OK': %q", status_line)
end
-- strip status line from payload
val = string.gsub(val, "HTTP/1.1 200 OK\r\n", "", 1)
for k, v in string.gmatch(val, "([%g]+): ([%g ]*)\r\n") do

local info = {}
-- iterate line-by-line by splitting on `\r\n`
for l in string.gmatch(val, "([^\r\n]*)\r\n") do
if l == nil or l == "" then
break
end
-- SSDP Messages use the HTTP/1.1 Header Field rules described in RFC 2616, 4.2: https://datatracker.ietf.org/doc/html/rfc2616#section-4.2
-- This pattern extracts the Key/Value pairs in to a Lua table via the two capture groups.
-- The key capture group is composed entirely of a negating matcher to exclude illegal characters, ending at the `:`.
-- The RFC states that after the colon there may be any arbitrary amount of leading space between the colon
-- and the value, and that the value shouldn't have any trailing whitespace, so we exclude those as well.
-- The original Luncheon implementation of this Lua Pattern used iteration and detected the `;` separator
-- that indicates key/value parameters, however, we don't make that distinction here and instead leave parsing
-- values with parameters to the consumers of the output of this function.
local k, v = string.match(l, '([^%c()<>@,;:\\"/%[%]?={} \t]+):%s*(.-)%s*$')
if k == nil or k == "" then
return nil, string.format("Couldn't parse header/value pair for line %q", l)
end
info[string.lower(k)] = v
end
return info
Expand All @@ -21,7 +44,10 @@ function SSDP.check_headers_contain(response, ...)
local header_vals = table.pack(...)
for _, header in ipairs(header_vals) do
if header ~= nil then
if not response[header] then return false end
if not response[header] then
log.warn("No header available for key " .. st_utils.stringify_table(header))
return false
end
end
end
return true
Expand Down Expand Up @@ -74,12 +100,28 @@ function SSDP.search(search_term, callback)
local val, rip, _ = s:receivefrom()

if val then
local headers = process_response(val)
local headers, err = process_response(val)

if err ~= nil then
log.error(err or "Unknown error while parsing SSDP response headers")
goto continue
end

if headers == nil then
log.error("No headers found in SSDP response")
goto continue
end

if headers["st"] ~= search_term then
log.trace("Received SSDP response for different search term, skipping.")
goto continue
end

-- log all SSDP responses, even if they don't have proper headers
-- log all parseable SSDP responses for the search term,
-- even if they don't have proper headers.
log.debug_with({ hub_logs = true },
string.format("Received response for Sonos search with headers [%s], processing details",
st_utils.stringify_table(headers)))
string.format("Received response for Sonos search with headers [%s], processing details",
st_utils.stringify_table(headers)))
if
-- we don't explicitly check "st" because we don't index in to the contained
-- value so the equality check suffices as a nil check as well.
Expand All @@ -89,8 +131,7 @@ function SSDP.search(search_term, callback)
"location",
"groupinfo.smartspeaker.audio",
"websock.smartspeaker.audio",
"household.smartspeaker.audio") and
headers["st"] == search_term and headers["server"]:find("Sonos")
"household.smartspeaker.audio") and headers["server"]:find("Sonos")
then
local ip =
headers["location"]:match("http://([^,/]+):[^/]+/.+%.xml")
Expand Down Expand Up @@ -145,6 +186,7 @@ function SSDP.search(search_term, callback)
"error receiving discovery replies for search term: %s",
rip))
end
::continue::
end
s:close()
end
Expand Down

0 comments on commit c8038a4

Please sign in to comment.