From 8cf0cea081169479251e99b7a298f9d221e1d73c Mon Sep 17 00:00:00 2001
From: Carter Swedal <carter.swedal@smartthings.com>
Date: Mon, 11 Dec 2023 13:09:46 -0600
Subject: [PATCH 1/5] Fixup unit test for 51 wakeup notification bugfix (#1107)

---
 .../src/test/test_fibaro_smoke_sensor.lua          | 14 --------------
 1 file changed, 14 deletions(-)

diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_smoke_sensor.lua b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_smoke_sensor.lua
index c8a78e7f10..ebb50eaa6e 100644
--- a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_smoke_sensor.lua
+++ b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_smoke_sensor.lua
@@ -21,7 +21,6 @@ local t_utils = require "integration_test.utils"
 local Battery = (require "st.zwave.CommandClass.Battery")({ version=1 })
 local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=1 })
 local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({ version=5 })
-local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({ version = 2 })
 local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version=1 })
 local Notification = (require "st.zwave.CommandClass.Notification")({ version=4 })
 
@@ -159,19 +158,6 @@ test.register_coroutine_test(
         Configuration:Set({parameter_number=32, size=2, configuration_value=4320})
       )
     )
-    test.socket.zwave:__expect_send(
-      zw_test_utils.zwave_test_build_send_command(
-        mock_device,
-        SensorBinary:Get({sensor_type = SensorBinary.sensor_type.FREEZE})
-      )
-    )
-    test.socket.zwave:__expect_send(
-      zw_test_utils.zwave_test_build_send_command(
-        mock_device,
-        SensorBinary:Get({sensor_type = SensorBinary.sensor_type.SMOKE})
-      )
-    )
-
   end
 )
 

From d877f2fcc6b5daa1416bcd714f8815e4a542ca08 Mon Sep 17 00:00:00 2001
From: Doug Stephen <doug.stephen@smartthings.com>
Date: Fri, 8 Dec 2023 17:48:49 -0600
Subject: [PATCH 2/5] Fix parsing patterns for SSDP responses

The current way we parse the SSDP response headers can cause failures
in discovering Sonos speakers with unicode characters that require
more than one byte for their representation, such as CJK characters.

The current parsing patterns use the '`%g`' character class, which does
not parse characters that don't fit in a single byte; while Lua itself
is utf-8 compatible and it is safe to use multi-code-point graphemes or
clusters in Lua strings, the actual string library is byte-oriented for
many operations and the pattern used currently tries to match each byte
against the provided class.

'`.-`' works here instead, as it matches all bytes in a non-greedy way
(akin to '`.*?`' in PCRE).
---
 drivers/SmartThings/sonos/src/ssdp.lua | 24 +++++++++++++++++++++---
 1 file changed, 21 insertions(+), 3 deletions(-)

diff --git a/drivers/SmartThings/sonos/src/ssdp.lua b/drivers/SmartThings/sonos/src/ssdp.lua
index 5cd2f85679..2b5ea5e8f3 100644
--- a/drivers/SmartThings/sonos/src/ssdp.lua
+++ b/drivers/SmartThings/sonos/src/ssdp.lua
@@ -8,9 +8,24 @@ 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
+    local k, v = string.match(l, "(.-):%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
@@ -21,7 +36,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

From dcf77db7d5d89c5869e9c63cf4fdffb1eda91fd4 Mon Sep 17 00:00:00 2001
From: Doug Stephen <doug.stephen@smartthings.com>
Date: Mon, 11 Dec 2023 12:19:10 -0600
Subject: [PATCH 3/5] Improve SSDP response parsing error handling.

---
 drivers/SmartThings/sonos/src/ssdp.lua | 28 ++++++++++++++++++++------
 1 file changed, 22 insertions(+), 6 deletions(-)

diff --git a/drivers/SmartThings/sonos/src/ssdp.lua b/drivers/SmartThings/sonos/src/ssdp.lua
index 2b5ea5e8f3..0234d01f3c 100644
--- a/drivers/SmartThings/sonos/src/ssdp.lua
+++ b/drivers/SmartThings/sonos/src/ssdp.lua
@@ -92,12 +92,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)
 
-      -- log all SSDP responses, even if they don't have proper headers
+      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 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.
@@ -107,8 +123,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")
@@ -163,6 +178,7 @@ function SSDP.search(search_term, callback)
         "error receiving discovery replies for search term: %s",
         rip))
     end
+    ::continue::
   end
   s:close()
 end

From 5669aa968dd37f240005ec9216bc8d01beda7804 Mon Sep 17 00:00:00 2001
From: Cooper Towns <cooper.towns@smartthings.com>
Date: Mon, 11 Dec 2023 11:42:47 -0600
Subject: [PATCH 4/5] Matter Button: add 6-button static profile support

Add profile to support 6-button matter button devices. This
means 6-button devieces will be supported as MCD rather than
parent-child.
---
 .../profiles/6-button-battery.yml             | 44 +++++++++++++++++++
 .../matter-button/profiles/6-button.yml       | 42 ++++++++++++++++++
 .../SmartThings/matter-button/src/init.lua    |  2 +-
 3 files changed, 87 insertions(+), 1 deletion(-)
 create mode 100644 drivers/SmartThings/matter-button/profiles/6-button-battery.yml
 create mode 100644 drivers/SmartThings/matter-button/profiles/6-button.yml

diff --git a/drivers/SmartThings/matter-button/profiles/6-button-battery.yml b/drivers/SmartThings/matter-button/profiles/6-button-battery.yml
new file mode 100644
index 0000000000..8ec3642e81
--- /dev/null
+++ b/drivers/SmartThings/matter-button/profiles/6-button-battery.yml
@@ -0,0 +1,44 @@
+name: 6-button-battery
+components:
+  - id: main
+    capabilities:
+      - id: button
+        version: 1
+      - id: battery
+        version: 1
+      - id: firmwareUpdate
+        version: 1
+      - id: refresh
+        version: 1
+    categories:
+    - name: RemoteController
+  - id: button2
+    capabilities:
+      - id: button
+        version: 1
+    categories:
+    - name: RemoteController
+  - id: button3
+    capabilities:
+      - id: button
+        version: 1
+    categories:
+    - name: RemoteController
+  - id: button4
+    capabilities:
+      - id: button
+        version: 1
+    categories:
+    - name: RemoteController
+  - id: button5
+    capabilities:
+      - id: button
+        version: 1
+    categories:
+    - name: RemoteController
+  - id: button6
+    capabilities:
+      - id: button
+        version: 1
+    categories:
+    - name: RemoteController
diff --git a/drivers/SmartThings/matter-button/profiles/6-button.yml b/drivers/SmartThings/matter-button/profiles/6-button.yml
new file mode 100644
index 0000000000..248ddd2bcb
--- /dev/null
+++ b/drivers/SmartThings/matter-button/profiles/6-button.yml
@@ -0,0 +1,42 @@
+name: 6-button
+components:
+  - id: main
+    capabilities:
+      - id: button
+        version: 1
+      - id: firmwareUpdate
+        version: 1
+      - id: refresh
+        version: 1
+    categories:
+    - name: RemoteController
+  - id: button2
+    capabilities:
+      - id: button
+        version: 1
+    categories:
+    - name: RemoteController
+  - id: button3
+    capabilities:
+      - id: button
+        version: 1
+    categories:
+    - name: RemoteController
+  - id: button4
+    capabilities:
+      - id: button
+        version: 1
+    categories:
+    - name: RemoteController
+  - id: button5
+    capabilities:
+      - id: button
+        version: 1
+    categories:
+    - name: RemoteController
+  - id: button6
+    capabilities:
+      - id: button
+        version: 1
+    categories:
+    - name: RemoteController
diff --git a/drivers/SmartThings/matter-button/src/init.lua b/drivers/SmartThings/matter-button/src/init.lua
index 689fcbd3d2..d78812807d 100644
--- a/drivers/SmartThings/matter-button/src/init.lua
+++ b/drivers/SmartThings/matter-button/src/init.lua
@@ -9,7 +9,7 @@ local START_BUTTON_PRESS = "__start_button_press"
 local TIMEOUT_THRESHOLD = 10 --arbitrary timeout
 local HELD_THRESHOLD = 1
 -- this is the number of buttons for which we have a static profile already made
-local STATIC_PROFILE_SUPPORTED = {2, 4, 8}
+local STATIC_PROFILE_SUPPORTED = {2, 4, 6, 8}
 
 local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map"
 local DEFERRED_CONFIGURE = "__DEFERRED_CONFIGURE"

From 7e4e948968a946546303e321b2105e70516a2362 Mon Sep 17 00:00:00 2001
From: Doug Stephen <doug.stephen@smartthings.com>
Date: Mon, 11 Dec 2023 12:51:59 -0600
Subject: [PATCH 5/5] Use a more conservative parsing approach

Previous approach could have potentially accepted invalid headers. This
tweaks the parsing pattern to fail to find headers that don't coform to
the actual accepted structure of a header key.
---
 drivers/SmartThings/sonos/src/ssdp.lua | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/drivers/SmartThings/sonos/src/ssdp.lua b/drivers/SmartThings/sonos/src/ssdp.lua
index 0234d01f3c..d2308687ad 100644
--- a/drivers/SmartThings/sonos/src/ssdp.lua
+++ b/drivers/SmartThings/sonos/src/ssdp.lua
@@ -22,7 +22,15 @@ local function process_response(val)
     if l == nil or l == "" then
       break
     end
-    local k, v = string.match(l, "(.-):%s*(.*)$")
+    -- 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