|
|
@@ -9882,6 +9882,52 @@ private:
|
|
|
bool readable_hint_ = false;
|
|
|
};
|
|
|
|
|
|
+// A TLS stream for WebSocket connections, where the receive path and the
|
|
|
+// send path (application send() plus the heartbeat ping thread) run on
|
|
|
+// different threads. A single TLS session must never be entered
|
|
|
+// concurrently, so every call into the session is serialized by one mutex.
|
|
|
+//
|
|
|
+// Unlike SSLSocketStream, the socket is kept non-blocking for the stream's
|
|
|
+// whole lifetime and each read()/write() performs a single non-blocking TLS
|
|
|
+// call under the lock, then waits for readiness with select() outside the
|
|
|
+// lock. The lock is therefore held only for CPU-bound work, so a reader
|
|
|
+// blocked waiting for data never stalls a concurrent sender.
|
|
|
+//
|
|
|
+// This stream is used only for wss:// connections. Plain ws:// and ordinary
|
|
|
+// HTTP/HTTPS keep using SocketStream/SSLSocketStream unchanged.
|
|
|
+class WebSocketSSLStream final : public Stream {
|
|
|
+public:
|
|
|
+ WebSocketSSLStream(socket_t sock, tls::session_t session,
|
|
|
+ time_t read_timeout_sec, time_t read_timeout_usec,
|
|
|
+ time_t write_timeout_sec, time_t write_timeout_usec);
|
|
|
+ ~WebSocketSSLStream() override;
|
|
|
+
|
|
|
+ bool is_readable() const override;
|
|
|
+ bool wait_readable() const override;
|
|
|
+ bool wait_writable() const override;
|
|
|
+ ssize_t read(char *ptr, size_t size) override;
|
|
|
+ ssize_t write(const char *ptr, size_t size) override;
|
|
|
+ void get_remote_ip_and_port(std::string &ip, int &port) const override;
|
|
|
+ void get_local_ip_and_port(std::string &ip, int &port) const override;
|
|
|
+ socket_t socket() const override;
|
|
|
+ time_t duration() const override;
|
|
|
+ void set_read_timeout(time_t sec, time_t usec = 0) override;
|
|
|
+
|
|
|
+private:
|
|
|
+ mutable std::mutex session_mutex_;
|
|
|
+
|
|
|
+ socket_t sock_;
|
|
|
+ tls::session_t session_;
|
|
|
+ // WebSocket::close() shortens the read timeout from the closing thread
|
|
|
+ // while the receive thread is inside wait_readable(), so these two are read
|
|
|
+ // and written concurrently. The write timeouts are never mutated.
|
|
|
+ std::atomic<time_t> read_timeout_sec_;
|
|
|
+ std::atomic<time_t> read_timeout_usec_;
|
|
|
+ time_t write_timeout_sec_;
|
|
|
+ time_t write_timeout_usec_;
|
|
|
+ const std::chrono::time_point<std::chrono::steady_clock> start_time_;
|
|
|
+};
|
|
|
+
|
|
|
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
|
|
inline std::string message_digest(const std::string &s, const EVP_MD *algo) {
|
|
|
auto context = std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)>(
|
|
|
@@ -12182,6 +12228,127 @@ inline void SSLSocketStream::set_read_timeout(time_t sec, time_t usec) {
|
|
|
read_timeout_usec_ = usec;
|
|
|
}
|
|
|
|
|
|
+inline WebSocketSSLStream::WebSocketSSLStream(socket_t sock,
|
|
|
+ tls::session_t session,
|
|
|
+ time_t read_timeout_sec,
|
|
|
+ time_t read_timeout_usec,
|
|
|
+ time_t write_timeout_sec,
|
|
|
+ time_t write_timeout_usec)
|
|
|
+ : sock_(sock), session_(session), read_timeout_sec_(read_timeout_sec),
|
|
|
+ read_timeout_usec_(read_timeout_usec),
|
|
|
+ write_timeout_sec_(write_timeout_sec),
|
|
|
+ write_timeout_usec_(write_timeout_usec),
|
|
|
+ start_time_(std::chrono::steady_clock::now()) {
|
|
|
+ // The receive and send paths run on different threads, so each TLS call is
|
|
|
+ // driven in non-blocking mode and readiness is awaited with select()
|
|
|
+ // outside the session lock. Set the socket non-blocking once here; it is
|
|
|
+ // never flipped back, so no thread races on the flag.
|
|
|
+ detail::set_nonblocking(sock_, true);
|
|
|
+#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
|
|
+ SSL_clear_mode(static_cast<SSL *>(session_), SSL_MODE_AUTO_RETRY);
|
|
|
+#endif
|
|
|
+}
|
|
|
+
|
|
|
+inline WebSocketSSLStream::~WebSocketSSLStream() = default;
|
|
|
+
|
|
|
+inline bool WebSocketSSLStream::is_readable() const {
|
|
|
+ std::lock_guard<std::mutex> guard(session_mutex_);
|
|
|
+ return tls::pending(session_) > 0;
|
|
|
+}
|
|
|
+
|
|
|
+inline bool WebSocketSSLStream::wait_readable() const {
|
|
|
+ return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0;
|
|
|
+}
|
|
|
+
|
|
|
+inline bool WebSocketSSLStream::wait_writable() const {
|
|
|
+ // Unlike SSLSocketStream, this deliberately does not call is_peer_closed():
|
|
|
+ // that probe toggles the socket's blocking flag, which would race with the
|
|
|
+ // concurrent reader on a permanently non-blocking socket.
|
|
|
+ return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0;
|
|
|
+}
|
|
|
+
|
|
|
+inline ssize_t WebSocketSSLStream::read(char *ptr, size_t size) {
|
|
|
+ tls::TlsError err;
|
|
|
+ auto n = 1000;
|
|
|
+ while (--n >= 0) {
|
|
|
+ {
|
|
|
+ std::lock_guard<std::mutex> guard(session_mutex_);
|
|
|
+ auto ret = tls::read(session_, ptr, size, err);
|
|
|
+ if (ret > 0) { return ret; }
|
|
|
+ if (ret == 0 || err.code == tls::ErrorCode::PeerClosed) {
|
|
|
+ error_ = Error::ConnectionClosed;
|
|
|
+ return ret;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // ret < 0. On a non-blocking socket a TLS read can stop needing either
|
|
|
+ // direction: the send path shares this session, so output it left pending
|
|
|
+ // has to be flushed before more input can be decrypted. Anything else is
|
|
|
+ // a hard error.
|
|
|
+ auto needs_readable = err.code == tls::ErrorCode::WantRead;
|
|
|
+#ifdef _WIN32
|
|
|
+ // On Windows a socket timeout surfaces as a syscall error, not WantRead.
|
|
|
+ needs_readable =
|
|
|
+ needs_readable || (err.code == tls::ErrorCode::SyscallError &&
|
|
|
+ WSAGetLastError() == WSAETIMEDOUT);
|
|
|
+#endif
|
|
|
+ if (!needs_readable && err.code != tls::ErrorCode::WantWrite) { return -1; }
|
|
|
+ if (!(needs_readable ? wait_readable() : wait_writable())) {
|
|
|
+ error_ = Error::Timeout;
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return -1;
|
|
|
+}
|
|
|
+
|
|
|
+inline ssize_t WebSocketSSLStream::write(const char *ptr, size_t size) {
|
|
|
+ auto handle_size = std::min<size_t>(size, (std::numeric_limits<int>::max)());
|
|
|
+ tls::TlsError err;
|
|
|
+ auto n = 1000;
|
|
|
+ while (--n >= 0) {
|
|
|
+ {
|
|
|
+ std::lock_guard<std::mutex> guard(session_mutex_);
|
|
|
+ auto ret = tls::write(session_, ptr, handle_size, err);
|
|
|
+ if (ret >= 0) { return ret; }
|
|
|
+ }
|
|
|
+ // ret < 0. As in read(), either direction can be needed: a renegotiation
|
|
|
+ // or a post-handshake message must be consumed before the record goes
|
|
|
+ // out. Anything else is a hard error.
|
|
|
+ auto needs_writable = err.code == tls::ErrorCode::WantWrite;
|
|
|
+#ifdef _WIN32
|
|
|
+ // On Windows a socket timeout surfaces as a syscall error, not WantWrite.
|
|
|
+ needs_writable =
|
|
|
+ needs_writable || (err.code == tls::ErrorCode::SyscallError &&
|
|
|
+ WSAGetLastError() == WSAETIMEDOUT);
|
|
|
+#endif
|
|
|
+ if (!needs_writable && err.code != tls::ErrorCode::WantRead) { return -1; }
|
|
|
+ if (!(needs_writable ? wait_writable() : wait_readable())) { return -1; }
|
|
|
+ }
|
|
|
+ return -1;
|
|
|
+}
|
|
|
+
|
|
|
+inline void WebSocketSSLStream::get_remote_ip_and_port(std::string &ip,
|
|
|
+ int &port) const {
|
|
|
+ detail::get_remote_ip_and_port(sock_, ip, port);
|
|
|
+}
|
|
|
+
|
|
|
+inline void WebSocketSSLStream::get_local_ip_and_port(std::string &ip,
|
|
|
+ int &port) const {
|
|
|
+ detail::get_local_ip_and_port(sock_, ip, port);
|
|
|
+}
|
|
|
+
|
|
|
+inline socket_t WebSocketSSLStream::socket() const { return sock_; }
|
|
|
+
|
|
|
+inline time_t WebSocketSSLStream::duration() const {
|
|
|
+ return std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
|
+ std::chrono::steady_clock::now() - start_time_)
|
|
|
+ .count();
|
|
|
+}
|
|
|
+
|
|
|
+inline void WebSocketSSLStream::set_read_timeout(time_t sec, time_t usec) {
|
|
|
+ read_timeout_sec_ = sec;
|
|
|
+ read_timeout_usec_ = usec;
|
|
|
+}
|
|
|
+
|
|
|
} // namespace detail
|
|
|
#endif // CPPHTTPLIB_SSL_ENABLED
|
|
|
|
|
|
@@ -13673,6 +13840,24 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|
|
if (websocket_upgraded) { *websocket_upgraded = true; }
|
|
|
|
|
|
{
|
|
|
+#ifdef CPPHTTPLIB_SSL_ENABLED
|
|
|
+ if (req.ssl) {
|
|
|
+ // wss: the heartbeat ping thread and the read path enter the same
|
|
|
+ // TLS session from different threads. Hand the WebSocket a stream
|
|
|
+ // that serializes every TLS call, so the shared SSLSocketStream on
|
|
|
+ // the plain HTTP/HTTPS paths stays untouched.
|
|
|
+ auto ws_strm =
|
|
|
+ std::unique_ptr<Stream>(new detail::WebSocketSSLStream(
|
|
|
+ strm.socket(), const_cast<tls::session_t>(req.ssl),
|
|
|
+ CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND, 0,
|
|
|
+ write_timeout_sec_, write_timeout_usec_));
|
|
|
+ ws::WebSocket ws(std::move(ws_strm), req, true,
|
|
|
+ websocket_ping_interval_sec_,
|
|
|
+ websocket_max_missed_pongs_);
|
|
|
+ entry.handler(req, ws);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+#endif
|
|
|
// Use WebSocket-specific read timeout instead of HTTP timeout
|
|
|
strm.set_read_timeout(CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND, 0);
|
|
|
ws::WebSocket ws(strm, req, true, websocket_ping_interval_sec_,
|
|
|
@@ -21765,7 +21950,7 @@ inline bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm,
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
- strm = std::unique_ptr<Stream>(new detail::SSLSocketStream(
|
|
|
+ strm = std::unique_ptr<Stream>(new detail::WebSocketSSLStream(
|
|
|
sock_, tls_session_, read_timeout_sec_, read_timeout_usec_,
|
|
|
write_timeout_sec_, write_timeout_usec_));
|
|
|
return true;
|