2 Komitmen d59f3e438c ... 6d1049462d

Pembuat SHA1 Pesan Tanggal
  yhirose 6d1049462d Ignore a subprotocol the WebSocket client did not offer 3 minggu lalu
  metsw24-max 4fcbc08f2d reject an unoffered subprotocol in read_websocket_upgrade_response (#2595) 3 minggu lalu
3 mengubah file dengan 102 tambahan dan 5 penghapusan
  1. 1 1
      README-websocket.md
  2. 31 4
      httplib.h
  3. 70 0
      test/test.cc

+ 1 - 1
README-websocket.md

@@ -119,7 +119,7 @@ using SubProtocolSelector =
     std::function<std::string(const std::vector<std::string> &protocols)>;
 ```
 
-The `SubProtocolSelector` receives the list of subprotocols proposed by the client (from the `Sec-WebSocket-Protocol` header) and returns the selected one. Return an empty string to decline all proposed subprotocols.
+The `SubProtocolSelector` receives the list of subprotocols proposed by the client (from the `Sec-WebSocket-Protocol` header) and returns the selected one. Return an empty string to decline all proposed subprotocols. A returned value that the client did not propose is ignored.
 
 ### WebSocket (Server-side)
 

+ 31 - 4
httplib.h

@@ -8272,9 +8272,11 @@ struct WebSocketUpgradeResponse {
   std::string selected_subprotocol;
 };
 
-inline bool read_websocket_upgrade_response(Stream &strm,
-                                            const std::string &expected_accept,
-                                            WebSocketUpgradeResponse &upgrade) {
+inline bool
+read_websocket_upgrade_response(Stream &strm,
+                                const std::string &expected_accept,
+                                const std::string &offered_subprotocols,
+                                WebSocketUpgradeResponse &upgrade) {
   // Read status line
   const auto bufsiz = 2048;
   char buf[bufsiz];
@@ -8331,6 +8333,22 @@ inline bool read_websocket_upgrade_response(Stream &strm,
     upgrade.selected_subprotocol = proto_it->second;
   }
 
+  // Verify the subprotocol is one the client offered (RFC 6455 4.1)
+  if (!upgrade.selected_subprotocol.empty()) {
+    auto was_offered = false;
+    split(offered_subprotocols.data(),
+          offered_subprotocols.data() + offered_subprotocols.size(), ',',
+          [&](const char *b, const char *e) {
+            if (std::string(b, e) == upgrade.selected_subprotocol) {
+              was_offered = true;
+            }
+          });
+    if (!was_offered) {
+      upgrade.error = Error::WebSocketHandshake;
+      return false;
+    }
+  }
+
   return true;
 }
 
@@ -10328,7 +10346,10 @@ inline bool perform_websocket_handshake(Stream &strm, Request &req,
 
   // Verify 101 response and Sec-WebSocket-Accept header
   auto expected_accept = websocket_accept_key(client_key);
-  return read_websocket_upgrade_response(strm, expected_accept, upgrade);
+  auto offered_subprotocols =
+      get_combined_header_value(req.headers, "Sec-WebSocket-Protocol");
+  return read_websocket_upgrade_response(strm, expected_accept,
+                                         offered_subprotocols, upgrade);
 }
 
 inline bool is_ip_address(const std::string &host) {
@@ -14671,6 +14692,12 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
                             protocols.emplace_back(b, e);
                           });
             selected_subprotocol = entry.sub_protocol_selector(protocols);
+
+            // Ignore a selection the client did not offer (RFC 6455 4.2.2)
+            if (std::find(protocols.begin(), protocols.end(),
+                          selected_subprotocol) == protocols.end()) {
+              selected_subprotocol.clear();
+            }
           }
         }
 

+ 70 - 0
test/test.cc

@@ -24061,6 +24061,18 @@ protected:
           }
           return "";
         });
+
+    server_->WebSocket(
+        "/ws-subprotocol-unoffered",
+        [](const Request &, ws::WebSocket &ws) {
+          std::string msg;
+          while (ws.read(msg)) {
+            ws.send(msg);
+          }
+        },
+        [](const std::vector<std::string> &) -> std::string {
+          return "admin";
+        });
   }
 
   void start_server() {
@@ -24403,6 +24415,18 @@ TEST_F(WebSocketIntegrationTest, SubProtocolNoMatch) {
   client.close();
 }
 
+TEST_F(WebSocketIntegrationTest, SubProtocolSelectorReturnsUnoffered) {
+  Headers headers = {{"Sec-WebSocket-Protocol", "chat"}};
+  ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) +
+                                 "/ws-subprotocol-unoffered",
+                             headers);
+  ASSERT_TRUE(client.connect());
+
+  EXPECT_TRUE(client.subprotocol().empty());
+
+  client.close();
+}
+
 TEST_F(WebSocketIntegrationTest, SubProtocolNotRequested) {
   // Connect without requesting any subprotocol
   ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) +
@@ -25001,6 +25025,52 @@ TEST(WebSocketTest, ClientRejectsResponseWithoutUpgradeToken) {
   EXPECT_FALSE(client.is_open());
 }
 
+TEST(WebSocketTest, ClientRejectsUnofferedSubprotocol) {
+  Server svr;
+  svr.Get("/ws", [](const Request &req, Response &res) {
+    res.status = StatusCode::SwitchingProtocol_101;
+    res.set_header("Upgrade", "websocket");
+    res.set_header("Connection", "Upgrade");
+    res.set_header("Sec-WebSocket-Accept",
+                   detail::websocket_accept_key(
+                       req.get_header_value("Sec-WebSocket-Key")));
+    res.set_header("Sec-WebSocket-Protocol", "admin");
+  });
+
+  auto port = svr.bind_to_any_port("localhost");
+  std::thread t([&]() { svr.listen_after_bind(); });
+  auto se = detail::scope_exit([&] {
+    svr.stop();
+    t.join();
+  });
+  svr.wait_until_ready();
+
+  const auto url = "ws://localhost:" + std::to_string(port) + "/ws";
+
+  // Server selects a subprotocol the client never offered
+  {
+    Headers headers = {{"Sec-WebSocket-Protocol", "chat"}};
+    ws::WebSocketClient client(url, headers);
+
+    auto res = client.connect();
+    EXPECT_FALSE(res);
+    EXPECT_EQ(Error::WebSocketHandshake, res.error());
+    EXPECT_FALSE(client.is_open());
+    EXPECT_TRUE(client.subprotocol().empty());
+  }
+
+  // Client offered none but the server named one anyway
+  {
+    ws::WebSocketClient client(url);
+
+    auto res = client.connect();
+    EXPECT_FALSE(res);
+    EXPECT_EQ(Error::WebSocketHandshake, res.error());
+    EXPECT_FALSE(client.is_open());
+    EXPECT_TRUE(client.subprotocol().empty());
+  }
+}
+
 TEST(WebSocketTest, HostHeaderOverUnixSocket) {
   // The socket path doubles as the URL host, so it must not contain '/'.
   const char *shard = getenv("GTEST_SHARD_INDEX");