summaryrefslogtreecommitdiff
path: root/indra/llcorehttp
diff options
context:
space:
mode:
Diffstat (limited to 'indra/llcorehttp')
-rw-r--r--indra/llcorehttp/CMakeLists.txt27
-rw-r--r--indra/llcorehttp/_httplibcurl.h8
-rw-r--r--indra/llcorehttp/_httpoperation.h24
-rw-r--r--indra/llcorehttp/_httpoprequest.cpp23
-rw-r--r--indra/llcorehttp/_httpoprequest.h4
-rw-r--r--indra/llcorehttp/_httpopsetget.h4
-rw-r--r--indra/llcorehttp/_httppolicy.h4
-rw-r--r--indra/llcorehttp/_httppolicyclass.h2
-rw-r--r--indra/llcorehttp/_httppolicyglobal.h2
-rw-r--r--indra/llcorehttp/_httpreadyqueue.h4
-rw-r--r--indra/llcorehttp/_httpreplyqueue.h7
-rw-r--r--indra/llcorehttp/_httprequestqueue.h4
-rw-r--r--indra/llcorehttp/_httpservice.cpp51
-rw-r--r--indra/llcorehttp/_httpservice.h6
-rw-r--r--indra/llcorehttp/_mutex.h11
-rw-r--r--indra/llcorehttp/_refcounted.h12
-rw-r--r--indra/llcorehttp/_thread.h101
-rw-r--r--indra/llcorehttp/bufferarray.cpp6
-rw-r--r--indra/llcorehttp/bufferarray.h4
-rw-r--r--indra/llcorehttp/bufferstream.h4
-rw-r--r--indra/llcorehttp/examples/http_texture_load.cpp52
-rw-r--r--indra/llcorehttp/httpcommon.cpp6
-rw-r--r--indra/llcorehttp/httpcommon.h21
-rw-r--r--indra/llcorehttp/httpheaders.h10
-rw-r--r--indra/llcorehttp/httpoptions.cpp6
-rw-r--r--indra/llcorehttp/httpoptions.h17
-rw-r--r--indra/llcorehttp/httprequest.cpp30
-rw-r--r--indra/llcorehttp/httprequest.h8
-rw-r--r--indra/llcorehttp/httpresponse.h4
-rw-r--r--indra/llcorehttp/httpstats.cpp3
-rw-r--r--indra/llcorehttp/httpstats.h12
-rw-r--r--[-rwxr-xr-x]indra/llcorehttp/llhttpconstants.cpp2
-rw-r--r--[-rwxr-xr-x]indra/llcorehttp/llhttpconstants.h4
-rw-r--r--indra/llcorehttp/lljsonrpcws.cpp902
-rw-r--r--indra/llcorehttp/lljsonrpcws.h547
-rw-r--r--indra/llcorehttp/llwebsocketmgr.cpp776
-rw-r--r--indra/llcorehttp/llwebsocketmgr.h328
-rw-r--r--indra/llcorehttp/tests/jsonrpcws_test.cpp553
-rw-r--r--[-rwxr-xr-x]indra/llcorehttp/tests/llcorehttp_test.cpp59
-rw-r--r--indra/llcorehttp/tests/test_httprequest.hpp28
-rw-r--r--indra/llcorehttp/tests/test_jsonrpcws.hpp203
-rw-r--r--indra/llcorehttp/tests/test_refcounted.hpp3
-rw-r--r--indra/llcorehttp/tests/test_websocketmgr.hpp157
-rw-r--r--indra/llcorehttp/tests/testwsclient.h170
-rw-r--r--indra/llcorehttp/tests/wsserver_test.cpp438
45 files changed, 4324 insertions, 323 deletions
diff --git a/indra/llcorehttp/CMakeLists.txt b/indra/llcorehttp/CMakeLists.txt
index ab3a86b37e..058c3feac5 100644
--- a/indra/llcorehttp/CMakeLists.txt
+++ b/indra/llcorehttp/CMakeLists.txt
@@ -13,6 +13,7 @@ include(LLAddBuildTest)
endif ()
include(LLCommon)
include(bugsplat)
+include(websocketpp)
set(llcorehttp_SOURCE_FILES
bufferarray.cpp
@@ -24,6 +25,8 @@ set(llcorehttp_SOURCE_FILES
httprequest.cpp
httpresponse.cpp
httpstats.cpp
+ llwebsocketmgr.cpp
+ lljsonrpcws.cpp
_httplibcurl.cpp
_httpopcancel.cpp
_httpoperation.cpp
@@ -52,6 +55,8 @@ set(llcorehttp_HEADER_FILES
httprequest.h
httpresponse.h
httpstats.h
+ llwebsocketmgr.h
+ lljsonrpcws.h
_httpinternal.h
_httplibcurl.h
_httpopcancel.h
@@ -82,10 +87,12 @@ list(APPEND llcorehttp_SOURCE_FILES ${llcorehttp_HEADER_FILES})
add_library (llcorehttp ${llcorehttp_SOURCE_FILES})
target_link_libraries(
llcorehttp
+ llmath
llcommon
ll::libcurl
ll::openssl
ll::nghttp2
+ ll::websocketpp
)
target_include_directories( llcorehttp INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
# llmessage depends on llcorehttp, yet llcorehttp also depends on llmessage (at least for includes).
@@ -95,6 +102,10 @@ target_include_directories( llcorehttp PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../ll
include(LibraryInstall)
+if (USE_PRECOMPILED_HEADERS)
+ target_precompile_headers(llcorehttp REUSE_FROM llprecompiled)
+endif ()
+
# tests
set(LLCOREHTTP_TESTS ON CACHE BOOL
"Build and run llcorehttp integration tests specifically")
@@ -111,6 +122,8 @@ if (LL_TESTS AND LLCOREHTTP_TESTS)
tests/test_httpheaders.hpp
tests/test_bufferarray.hpp
tests/test_bufferstream.hpp
+ tests/test_jsonrpcws.hpp
+ tests/test_websocketmgr.hpp
)
list(APPEND llcorehttp_TEST_SOURCE_FILES ${llcorehttp_TEST_HEADER_FILES})
@@ -134,7 +147,11 @@ if (LL_TESTS AND LLCOREHTTP_TESTS)
${PYTHON_EXECUTABLE}
"${CMAKE_CURRENT_SOURCE_DIR}/tests/test_llcorehttp_peer.py"
)
-
+
+ LL_ADD_INTEGRATION_TEST(wsserver "" "${test_libs}")
+
+ LL_ADD_INTEGRATION_TEST(jsonrpcws "" "${test_libs}")
+
#
# Example Programs
#
@@ -154,6 +171,7 @@ if (LL_TESTS AND LLCOREHTTP_TESTS)
set_target_properties(http_texture_load
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${EXE_STAGING_DIR}"
+ FOLDER "Tests"
)
if (WINDOWS)
@@ -166,6 +184,13 @@ if (LL_TESTS AND LLCOREHTTP_TESTS)
)
endif (WINDOWS)
+ if(DARWIN)
+ set_target_properties(http_texture_load
+ PROPERTIES
+ OSX_ARCHITECTURES ${LL_MACOS_TEST_ARCHITECTURE}
+ )
+ endif()
+
target_link_libraries(http_texture_load ${example_libs})
endif (LL_TESTS AND LLCOREHTTP_TESTS)
diff --git a/indra/llcorehttp/_httplibcurl.h b/indra/llcorehttp/_httplibcurl.h
index 3631965837..58affcb796 100644
--- a/indra/llcorehttp/_httplibcurl.h
+++ b/indra/llcorehttp/_httplibcurl.h
@@ -61,8 +61,8 @@ public:
virtual ~HttpLibcurl();
private:
- HttpLibcurl(const HttpLibcurl &); // Not defined
- void operator=(const HttpLibcurl &); // Not defined
+ HttpLibcurl(const HttpLibcurl&) = delete;
+ void operator=(const HttpLibcurl&) = delete;
public:
typedef std::shared_ptr<HttpOpRequest> opReqPtr_t;
@@ -179,8 +179,8 @@ protected:
~HandleCache();
private:
- HandleCache(const HandleCache &); // Not defined
- void operator=(const HandleCache &); // Not defined
+ HandleCache(const HandleCache&) = delete;
+ void operator=(const HandleCache&) = delete;
public:
/// Allocate a curl handle for caller. May be freed using
diff --git a/indra/llcorehttp/_httpoperation.h b/indra/llcorehttp/_httpoperation.h
index ff7efe60e9..8412043aa5 100644
--- a/indra/llcorehttp/_httpoperation.h
+++ b/indra/llcorehttp/_httpoperation.h
@@ -68,8 +68,7 @@ class HttpService;
/// via queue-like interfaces that are thread compatible
/// and those interfaces establish the access rules.
-class HttpOperation : private boost::noncopyable,
- public std::enable_shared_from_this<HttpOperation>
+class HttpOperation : public std::enable_shared_from_this<HttpOperation>
{
public:
typedef std::shared_ptr<HttpOperation> ptr_t;
@@ -82,6 +81,9 @@ public:
/// Threading: called by any thread.
virtual ~HttpOperation(); // Use release()
+ // Non-copyable
+ HttpOperation(const HttpOperation&) = delete;
+ HttpOperation& operator=(const HttpOperation&) = delete;
public:
/// Register a reply queue and a handler for completion notifications.
@@ -220,12 +222,10 @@ class HttpOpStop : public HttpOperation
{
public:
HttpOpStop();
-
virtual ~HttpOpStop();
-private:
- HttpOpStop(const HttpOpStop &); // Not defined
- void operator=(const HttpOpStop &); // Not defined
+ HttpOpStop(const HttpOpStop &) = delete;
+ HttpOpStop& operator=(const HttpOpStop&) = delete;
public:
virtual void stageFromRequest(HttpService *);
@@ -242,12 +242,10 @@ class HttpOpNull : public HttpOperation
{
public:
HttpOpNull();
-
virtual ~HttpOpNull();
-private:
- HttpOpNull(const HttpOpNull &); // Not defined
- void operator=(const HttpOpNull &); // Not defined
+ HttpOpNull(const HttpOpNull&) = delete;
+ HttpOpNull& operator=(const HttpOpNull&) = delete;
public:
virtual void stageFromRequest(HttpService *);
@@ -264,12 +262,10 @@ public:
// 0 does a hard spin in the operation
// 1 does a soft spin continuously requeuing itself
HttpOpSpin(int mode);
-
virtual ~HttpOpSpin();
-private:
- HttpOpSpin(const HttpOpSpin &); // Not defined
- void operator=(const HttpOpSpin &); // Not defined
+ HttpOpSpin(const HttpOpSpin&) = delete;
+ HttpOpSpin& operator=(const HttpOpSpin&) = delete;
public:
virtual void stageFromRequest(HttpService *);
diff --git a/indra/llcorehttp/_httpoprequest.cpp b/indra/llcorehttp/_httpoprequest.cpp
index 6186e7a308..bbf39c2afa 100644
--- a/indra/llcorehttp/_httpoprequest.cpp
+++ b/indra/llcorehttp/_httpoprequest.cpp
@@ -153,7 +153,7 @@ HttpOpRequest::HttpOpRequest()
mPolicyRetryLimit(HTTP_RETRY_COUNT_DEFAULT),
mPolicyMinRetryBackoff(HttpTime(HTTP_RETRY_BACKOFF_MIN_DEFAULT)),
mPolicyMaxRetryBackoff(HttpTime(HTTP_RETRY_BACKOFF_MAX_DEFAULT)),
- mCallbackSSLVerify(NULL)
+ mCallbackSSLVerify(nullptr)
{
// *NOTE: As members are added, retry initialization/cleanup
// may need to be extended in @see prepareRequest().
@@ -272,7 +272,7 @@ void HttpOpRequest::visitNotifier(HttpRequest * request)
response->setContentType(mReplyConType);
response->setRetries(mPolicyRetries, mPolicy503Retries);
- HttpResponse::TransferStats::ptr_t stats = HttpResponse::TransferStats::ptr_t(new HttpResponse::TransferStats);
+ HttpResponse::TransferStats::ptr_t stats = std::make_shared<HttpResponse::TransferStats>();
curl_easy_getinfo(mCurlHandle, CURLINFO_SIZE_DOWNLOAD, &stats->mSizeDownload);
curl_easy_getinfo(mCurlHandle, CURLINFO_TOTAL_TIME, &stats->mTotalTime);
@@ -513,7 +513,9 @@ HttpStatus HttpOpRequest::prepareRequest(HttpService * service)
check_curl_easy_setopt(mCurlHandle, CURLOPT_NOPROGRESS, 1);
check_curl_easy_setopt(mCurlHandle, CURLOPT_URL, mReqURL.c_str());
check_curl_easy_setopt(mCurlHandle, CURLOPT_PRIVATE, getHandle());
+#if LIBCURL_VERSION_MAJOR < 8
check_curl_easy_setopt(mCurlHandle, CURLOPT_ENCODING, "");
+#endif
check_curl_easy_setopt(mCurlHandle, CURLOPT_AUTOREFERER, 1);
check_curl_easy_setopt(mCurlHandle, CURLOPT_MAXREDIRS, HTTP_REDIRECTS_DEFAULT);
@@ -538,6 +540,7 @@ HttpStatus HttpOpRequest::prepareRequest(HttpService * service)
long sslHostV(0L);
long dnsCacheTimeout(-1L);
long nobody(0L);
+ curl_off_t lastModified(0L);
if (mReqOptions)
{
@@ -546,6 +549,7 @@ HttpStatus HttpOpRequest::prepareRequest(HttpService * service)
sslHostV = mReqOptions->getSSLVerifyHost() ? 2L : 0L;
dnsCacheTimeout = mReqOptions->getDNSCacheTimeout();
nobody = mReqOptions->getHeadersOnly() ? 1L : 0L;
+ lastModified = (curl_off_t)mReqOptions->getLastModified();
}
check_curl_easy_setopt(mCurlHandle, CURLOPT_FOLLOWLOCATION, follow_redirect);
@@ -554,6 +558,17 @@ HttpStatus HttpOpRequest::prepareRequest(HttpService * service)
check_curl_easy_setopt(mCurlHandle, CURLOPT_NOBODY, nobody);
+ if (lastModified)
+ {
+ check_curl_easy_setopt(mCurlHandle, CURLOPT_TIMECONDITION, CURL_TIMECOND_IFMODSINCE);
+#if (LIBCURL_VERSION_NUM >= 0x073B00)
+ // requires curl 7.59.0
+ check_curl_easy_setopt(mCurlHandle, CURLOPT_TIMEVALUE_LARGE, lastModified);
+#else
+ check_curl_easy_setopt(mCurlHandle, CURLOPT_TIMEVALUE, (long)lastModified);
+#endif
+ }
+
// The Linksys WRT54G V5 router has an issue with frequent
// DNS lookups from LAN machines. If they happen too often,
// like for every HTTP request, the router gets annoyed after
@@ -603,7 +618,9 @@ HttpStatus HttpOpRequest::prepareRequest(HttpService * service)
case HOR_POST:
{
check_curl_easy_setopt(mCurlHandle, CURLOPT_POST, 1);
+#if LIBCURL_VERSION_MAJOR < 8
check_curl_easy_setopt(mCurlHandle, CURLOPT_ENCODING, "");
+#endif
long data_size(0);
if (mReqBody)
{
@@ -951,7 +968,7 @@ size_t HttpOpRequest::headerCallback(void * data, size_t size, size_t nmemb, voi
// Save headers in response
if (! op->mReplyHeaders)
{
- op->mReplyHeaders = HttpHeaders::ptr_t(new HttpHeaders);
+ op->mReplyHeaders = std::make_shared<HttpHeaders>();
}
op->mReplyHeaders->append(name, value ? value : "");
}
diff --git a/indra/llcorehttp/_httpoprequest.h b/indra/llcorehttp/_httpoprequest.h
index b029bc740c..717535555c 100644
--- a/indra/llcorehttp/_httpoprequest.h
+++ b/indra/llcorehttp/_httpoprequest.h
@@ -73,8 +73,8 @@ public:
virtual ~HttpOpRequest(); // Use release()
private:
- HttpOpRequest(const HttpOpRequest &); // Not defined
- void operator=(const HttpOpRequest &); // Not defined
+ HttpOpRequest(const HttpOpRequest&) = delete;
+ void operator=(const HttpOpRequest&) = delete;
public:
enum EMethod
diff --git a/indra/llcorehttp/_httpopsetget.h b/indra/llcorehttp/_httpopsetget.h
index 0b927a6b71..ff8204547b 100644
--- a/indra/llcorehttp/_httpopsetget.h
+++ b/indra/llcorehttp/_httpopsetget.h
@@ -60,8 +60,8 @@ public:
virtual ~HttpOpSetGet(); // Use release()
private:
- HttpOpSetGet(const HttpOpSetGet &); // Not defined
- void operator=(const HttpOpSetGet &); // Not defined
+ HttpOpSetGet(const HttpOpSetGet&) = delete;
+ void operator=(const HttpOpSetGet&) = delete;
public:
/// Threading: called by application thread
diff --git a/indra/llcorehttp/_httppolicy.h b/indra/llcorehttp/_httppolicy.h
index a074949f20..82f552176e 100644
--- a/indra/llcorehttp/_httppolicy.h
+++ b/indra/llcorehttp/_httppolicy.h
@@ -56,8 +56,8 @@ public:
virtual ~HttpPolicy();
private:
- HttpPolicy(const HttpPolicy &); // Not defined
- void operator=(const HttpPolicy &); // Not defined
+ HttpPolicy(const HttpPolicy&) = delete;
+ void operator=(const HttpPolicy&) = delete;
public:
typedef std::shared_ptr<HttpOpRequest> opReqPtr_t;
diff --git a/indra/llcorehttp/_httppolicyclass.h b/indra/llcorehttp/_httppolicyclass.h
index 32bcad4f9c..5bf0fa1fa7 100644
--- a/indra/llcorehttp/_httppolicyclass.h
+++ b/indra/llcorehttp/_httppolicyclass.h
@@ -53,7 +53,7 @@ public:
~HttpPolicyClass();
HttpPolicyClass & operator=(const HttpPolicyClass &);
- HttpPolicyClass(const HttpPolicyClass &); // Not defined
+ HttpPolicyClass(const HttpPolicyClass &);
public:
HttpStatus set(HttpRequest::EPolicyOption opt, long value);
diff --git a/indra/llcorehttp/_httppolicyglobal.h b/indra/llcorehttp/_httppolicyglobal.h
index d9114d167f..3840d66b22 100644
--- a/indra/llcorehttp/_httppolicyglobal.h
+++ b/indra/llcorehttp/_httppolicyglobal.h
@@ -55,7 +55,7 @@ public:
HttpPolicyGlobal & operator=(const HttpPolicyGlobal &);
private:
- HttpPolicyGlobal(const HttpPolicyGlobal &); // Not defined
+ HttpPolicyGlobal(const HttpPolicyGlobal &) = delete;
public:
HttpStatus set(HttpRequest::EPolicyOption opt, long value);
diff --git a/indra/llcorehttp/_httpreadyqueue.h b/indra/llcorehttp/_httpreadyqueue.h
index 0bc0723511..04fcf25356 100644
--- a/indra/llcorehttp/_httpreadyqueue.h
+++ b/indra/llcorehttp/_httpreadyqueue.h
@@ -77,8 +77,8 @@ public:
{}
protected:
- HttpReadyQueue(const HttpReadyQueue &); // Not defined
- void operator=(const HttpReadyQueue &); // Not defined
+ HttpReadyQueue(const HttpReadyQueue&) = delete;
+ void operator=(const HttpReadyQueue&) = delete;
public:
diff --git a/indra/llcorehttp/_httpreplyqueue.h b/indra/llcorehttp/_httpreplyqueue.h
index d8847fafb5..e1b5648691 100644
--- a/indra/llcorehttp/_httpreplyqueue.h
+++ b/indra/llcorehttp/_httpreplyqueue.h
@@ -30,8 +30,6 @@
#include "_refcounted.h"
#include "_mutex.h"
-#include "boost/noncopyable.hpp"
-
namespace LLCore
{
@@ -59,7 +57,7 @@ class HttpOperation;
/// will be coded anyway so it shouldn't be too much of a
/// burden.
-class HttpReplyQueue : private boost::noncopyable
+class HttpReplyQueue
{
public:
@@ -69,6 +67,9 @@ public:
HttpReplyQueue();
virtual ~HttpReplyQueue();
+ HttpReplyQueue(const HttpReplyQueue&) = delete;
+ HttpReplyQueue& operator=(const HttpReplyQueue&) = delete;
+
public:
typedef std::vector< opPtr_t > OpContainer;
diff --git a/indra/llcorehttp/_httprequestqueue.h b/indra/llcorehttp/_httprequestqueue.h
index 0823126f78..82537c9053 100644
--- a/indra/llcorehttp/_httprequestqueue.h
+++ b/indra/llcorehttp/_httprequestqueue.h
@@ -57,8 +57,8 @@ protected:
virtual ~HttpRequestQueue(); // Use release()
private:
- HttpRequestQueue(const HttpRequestQueue &); // Not defined
- void operator=(const HttpRequestQueue &); // Not defined
+ HttpRequestQueue(const HttpRequestQueue&) = delete;
+ void operator=(const HttpRequestQueue&) = delete;
public:
typedef std::shared_ptr<HttpOperation> opPtr_t;
diff --git a/indra/llcorehttp/_httpservice.cpp b/indra/llcorehttp/_httpservice.cpp
index 5880fb7e87..e14a533479 100644
--- a/indra/llcorehttp/_httpservice.cpp
+++ b/indra/llcorehttp/_httpservice.cpp
@@ -27,7 +27,6 @@
#include "_httpservice.h"
#include <boost/bind.hpp>
-#include <boost/function.hpp>
#include "_httpoperation.h"
#include "_httprequestqueue.h"
@@ -71,7 +70,6 @@ volatile HttpService::EState HttpService::sState(NOT_INITIALIZED);
HttpService::HttpService()
: mRequestQueue(NULL),
mExitRequested(0U),
- mThread(NULL),
mPolicy(NULL),
mTransport(NULL),
mLastPolicy(0)
@@ -90,23 +88,30 @@ HttpService::~HttpService()
{
if (mRequestQueue->stopQueue())
{
- // Give mRequestQueue a chance to finish
- ms_sleep(10);
+ // Now wait a bit for the thread to exit
+ S32 counter = 0;
+ const S32 MAX_WAIT = 600;
+ while (counter < MAX_WAIT)
+ {
+ if (STOPPED == sState)
+ {
+ break;
+ }
+ // Sleep for a tenth of a second
+ ms_sleep(100);
+ std::this_thread::yield();
+ counter++;
+ }
}
}
- if (mThread)
+ if (mThread && RUNNING == sState)
{
- if (! mThread->timedJoin(250))
- {
- // Failed to join, expect problems ahead so do a hard termination.
- LL_WARNS(LOG_CORE) << "Destroying HttpService with running thread. Expect problems." << LL_NEWLINE
- << "State: " << S32(sState)
- << " Last policy: " << U32(mLastPolicy)
- << LL_ENDL;
+ // Failed to shutdown, expect problems ahead so do a hard termination.
+ LL_WARNS(LOG_CORE) << "Destroying HttpService with running thread. Expect problems." << LL_NEWLINE << "State: " << S32(sState)
+ << " Last policy: " << U32(mLastPolicy) << LL_ENDL;
- mThread->cancel();
- }
+ mThread->cancel();
}
}
@@ -122,11 +127,7 @@ HttpService::~HttpService()
delete mPolicy;
mPolicy = NULL;
- if (mThread)
- {
- mThread->release();
- mThread = NULL;
- }
+ mThread.reset();
}
@@ -205,14 +206,15 @@ void HttpService::startThread()
if (mThread)
{
- mThread->release();
+ mThread.reset();
}
// Push current policy definitions, enable policy & transport components
mPolicy->start();
mTransport->start(mLastPolicy + 1);
- mThread = new LLCoreInt::HttpThread(boost::bind(&HttpService::threadRun, this, _1));
+ mThread = std::make_unique<LLCoreInt::HttpThread>(boost::bind(&HttpService::threadRun, this, _1));
+ mThread->detach(); // Detach thread to let it clean its self up
sState = RUNNING;
}
@@ -283,12 +285,9 @@ void HttpService::shutdown()
// requested to stop.
void HttpService::threadRun(LLCoreInt::HttpThread * thread)
{
+ set_thread_name("HttpService");
LL_PROFILER_SET_THREAD_NAME("HttpService");
- boost::this_thread::disable_interruption di;
-
- LLThread::registerThreadID();
-
ELoopSpeed loop(REQUEST_SLEEP);
while (! mExitRequested)
{
@@ -315,7 +314,7 @@ void HttpService::threadRun(LLCoreInt::HttpThread * thread)
{
LOG_UNHANDLED_EXCEPTION("");
}
- catch (std::bad_alloc&)
+ catch (const std::bad_alloc&)
{
LLMemory::logMemoryInfo(true);
diff --git a/indra/llcorehttp/_httpservice.h b/indra/llcorehttp/_httpservice.h
index 13eb034f0e..4540fb9383 100644
--- a/indra/llcorehttp/_httpservice.h
+++ b/indra/llcorehttp/_httpservice.h
@@ -86,8 +86,8 @@ protected:
virtual ~HttpService();
private:
- HttpService(const HttpService &); // Not defined
- void operator=(const HttpService &); // Not defined
+ HttpService(const HttpService&) = delete;
+ void operator=(const HttpService&) = delete;
public:
enum EState
@@ -218,7 +218,7 @@ protected:
static volatile EState sState;
HttpRequestQueue * mRequestQueue; // Refcounted
LLAtomicU32 mExitRequested;
- LLCoreInt::HttpThread * mThread;
+ std::unique_ptr<LLCoreInt::HttpThread> mThread;
// === working-thread-only data ===
HttpPolicy * mPolicy; // Simple pointer, has ownership
diff --git a/indra/llcorehttp/_mutex.h b/indra/llcorehttp/_mutex.h
index 5e0164eb51..43782d3a37 100644
--- a/indra/llcorehttp/_mutex.h
+++ b/indra/llcorehttp/_mutex.h
@@ -27,9 +27,8 @@
#ifndef LLCOREINT_MUTEX_H_
#define LLCOREINT_MUTEX_H_
-
-#include <boost/thread.hpp>
-
+#include <condition_variable>
+#include <mutex>
namespace LLCoreInt
{
@@ -37,17 +36,17 @@ namespace LLCoreInt
// MUTEX TYPES
// unique mutex type
-typedef boost::mutex HttpMutex;
+typedef std::mutex HttpMutex;
// CONDITION VARIABLES
// standard condition variable
-typedef boost::condition_variable HttpConditionVariable;
+typedef std::condition_variable HttpConditionVariable;
// LOCKS AND FENCES
// scoped unique lock
-typedef boost::unique_lock<HttpMutex> HttpScopedLock;
+typedef std::unique_lock<HttpMutex> HttpScopedLock;
}
diff --git a/indra/llcorehttp/_refcounted.h b/indra/llcorehttp/_refcounted.h
index 7470965a7f..bc90f8274d 100644
--- a/indra/llcorehttp/_refcounted.h
+++ b/indra/llcorehttp/_refcounted.h
@@ -30,24 +30,20 @@
#include "linden_common.h"
-#include "fix_macros.h"
-#include <boost/thread.hpp>
#include <boost/intrusive_ptr.hpp>
-
#include "llatomic.h"
-
namespace LLCoreInt
{
class RefCounted
{
-private:
- RefCounted(); // Not defined - may not be default constructed
- void operator=(const RefCounted &); // Not defined
-
public:
+ RefCounted() = delete; // Not defined - may not be default constructed
+ RefCounted(const RefCounted&) = delete; // Not defined - may not be copy constructed
+ void operator=(const RefCounted&) = delete; // Not defined
+
explicit RefCounted(bool const implicit)
: mRefCount(implicit)
{}
diff --git a/indra/llcorehttp/_thread.h b/indra/llcorehttp/_thread.h
index 6c0e39cf92..66b9689c5a 100644
--- a/indra/llcorehttp/_thread.h
+++ b/indra/llcorehttp/_thread.h
@@ -29,92 +29,75 @@
#include "linden_common.h"
-#include <boost/thread.hpp>
-#include <boost/function.hpp>
-#include <boost/date_time/posix_time/posix_time_types.hpp>
+#include <functional>
+#include <thread>
-#include "apr.h" // thread-related functions
#include "_refcounted.h"
namespace LLCoreInt
{
-class HttpThread : public RefCounted
+class HttpThread
{
-private:
- HttpThread(); // Not defined
- void operator=(const HttpThread &); // Not defined
-
- void at_exit()
- {
- // the thread function has exited so we need to release our reference
- // to ourself so that we will be automagically cleaned up.
- release();
- }
+public:
+ HttpThread() = delete; // Not defined
+ HttpThread(const HttpThread&) = delete; // Not defined
+ void operator=(const HttpThread &) = delete; // Not defined
+private:
void run()
- { // THREAD CONTEXT
-
- // Take out additional reference for the at_exit handler
- addRef();
- boost::this_thread::at_thread_exit(boost::bind(&HttpThread::at_exit, this));
-
- // run the thread function
- mThreadFunc(this);
-
- } // THREAD CONTEXT
-
-protected:
- virtual ~HttpThread()
- {
- delete mThread;
- }
+ { // THREAD CONTEXT
+ // run the thread function
+ mThreadFunc(this);
+ } // THREAD CONTEXT
public:
/// Constructs a thread object for concurrent execution but does
/// not start running. Caller receives on refcount on the thread
/// instance. If the thread is started, another will be taken
/// out for the exit handler.
- explicit HttpThread(boost::function<void (HttpThread *)> threadFunc)
- : RefCounted(true), // implicit reference
- mThreadFunc(threadFunc)
- {
- // this creates a boost thread that will call HttpThread::run on this instance
- // and pass it the threadfunc callable...
- boost::function<void()> f = boost::bind(&HttpThread::run, this);
+ explicit HttpThread(std::function<void (HttpThread *)> threadFunc)
+ : mThreadFunc(threadFunc)
+ {
+ // this creates a std thread that will call HttpThread::run on this instance
+ // and pass it the threadfunc callable...
+ std::function<void()> f = std::bind(&HttpThread::run, this);
- mThread = new boost::thread(f);
- }
+ mThread = std::make_unique<std::thread>(f);
+ mNativeHandle = mThread->native_handle();
+ }
- inline void join()
- {
- mThread->join();
- }
+ ~HttpThread() = default;
- inline bool timedJoin(S32 millis)
- {
- return mThread->timed_join(boost::posix_time::milliseconds(millis));
- }
+ inline void join()
+ {
+ mThread->join();
+ }
inline bool joinable() const
- {
- return mThread->joinable();
- }
+ {
+ return mThread->joinable();
+ }
+
+ inline void detach()
+ {
+ mThread->detach();
+ }
// A very hostile method to force a thread to quit
inline void cancel()
- {
- boost::thread::native_handle_type thread(mThread->native_handle());
-#if LL_WINDOWS
- TerminateThread(thread, 0);
+ {
+#if LL_WINDOWS
+ TerminateThread(mNativeHandle, 0);
#else
- pthread_cancel(thread);
+ pthread_cancel(mNativeHandle);
#endif
- }
+ }
private:
- boost::function<void(HttpThread *)> mThreadFunc;
- boost::thread * mThread;
+ std::function<void(HttpThread *)> mThreadFunc;
+ std::unique_ptr<std::thread> mThread;
+ std::thread::native_handle_type mNativeHandle;
}; // end class HttpThread
} // end namespace LLCoreInt
diff --git a/indra/llcorehttp/bufferarray.cpp b/indra/llcorehttp/bufferarray.cpp
index 6b33661d8f..46c03f991d 100644
--- a/indra/llcorehttp/bufferarray.cpp
+++ b/indra/llcorehttp/bufferarray.cpp
@@ -57,12 +57,12 @@ public:
void operator delete(void *);
void operator delete(void *, size_t len);
+ Block(const Block&) = delete;
+ Block& operator=(const Block&) = delete;
+
protected:
Block(size_t len);
- Block(const Block &); // Not defined
- void operator=(const Block &); // Not defined
-
// Allocate the block with the additional space for the
// buffered data at the end of the object.
void * operator new(size_t len, size_t addl_len);
diff --git a/indra/llcorehttp/bufferarray.h b/indra/llcorehttp/bufferarray.h
index 5105dbc4f7..9abe1778ed 100644
--- a/indra/llcorehttp/bufferarray.h
+++ b/indra/llcorehttp/bufferarray.h
@@ -79,8 +79,8 @@ protected:
virtual ~BufferArray(); // Use release()
private:
- BufferArray(const BufferArray &); // Not defined
- void operator=(const BufferArray &); // Not defined
+ BufferArray(const BufferArray&) = delete;
+ void operator=(const BufferArray&) = delete;
public:
// Internal magic number, may be used by unit tests.
diff --git a/indra/llcorehttp/bufferstream.h b/indra/llcorehttp/bufferstream.h
index 93891810aa..ba84821df3 100644
--- a/indra/llcorehttp/bufferstream.h
+++ b/indra/llcorehttp/bufferstream.h
@@ -91,8 +91,8 @@ public:
virtual ~BufferArrayStreamBuf();
private:
- BufferArrayStreamBuf(const BufferArrayStreamBuf &); // Not defined
- void operator=(const BufferArrayStreamBuf &); // Not defined
+ BufferArrayStreamBuf(const BufferArrayStreamBuf&) = delete;
+ void operator=(const BufferArrayStreamBuf&) = delete;
public:
// Input interfaces from std::streambuf
diff --git a/indra/llcorehttp/examples/http_texture_load.cpp b/indra/llcorehttp/examples/http_texture_load.cpp
index 72e0c29a24..986e675d00 100644
--- a/indra/llcorehttp/examples/http_texture_load.cpp
+++ b/indra/llcorehttp/examples/http_texture_load.cpp
@@ -52,8 +52,6 @@
void init_curl();
void term_curl();
-void ssl_thread_id_callback(CRYPTO_THREADID*);
-void ssl_locking_callback(int mode, int type, const char * file, int line);
void usage(std::ostream & out);
// Default command line settings
@@ -606,63 +604,15 @@ void WorkingSet::loadAssetUuids(FILE * in)
}
-int ssl_mutex_count(0);
-LLCoreInt::HttpMutex ** ssl_mutex_list = NULL;
-
void init_curl()
{
curl_global_init(CURL_GLOBAL_ALL);
-
- ssl_mutex_count = CRYPTO_num_locks();
- if (ssl_mutex_count > 0)
- {
- ssl_mutex_list = new LLCoreInt::HttpMutex * [ssl_mutex_count];
-
- for (int i(0); i < ssl_mutex_count; ++i)
- {
- ssl_mutex_list[i] = new LLCoreInt::HttpMutex;
- }
-
- CRYPTO_set_locking_callback(ssl_locking_callback);
- CRYPTO_THREADID_set_callback(ssl_thread_id_callback);
- }
}
void term_curl()
{
- CRYPTO_set_locking_callback(NULL);
- for (int i(0); i < ssl_mutex_count; ++i)
- {
- delete ssl_mutex_list[i];
- }
- delete [] ssl_mutex_list;
-}
-
-
-void ssl_thread_id_callback(CRYPTO_THREADID* pthreadid)
-{
-#if defined(WIN32)
- CRYPTO_THREADID_set_pointer(pthreadid, GetCurrentThread());
-#else
- CRYPTO_THREADID_set_pointer(pthreadid, pthread_self());
-#endif
-}
-
-
-void ssl_locking_callback(int mode, int type, const char * /* file */, int /* line */)
-{
- if (type >= 0 && type < ssl_mutex_count)
- {
- if (mode & CRYPTO_LOCK)
- {
- ssl_mutex_list[type]->lock();
- }
- else
- {
- ssl_mutex_list[type]->unlock();
- }
- }
+ curl_global_cleanup();
}
diff --git a/indra/llcorehttp/httpcommon.cpp b/indra/llcorehttp/httpcommon.cpp
index 315ff15ebb..1037855017 100644
--- a/indra/llcorehttp/httpcommon.cpp
+++ b/indra/llcorehttp/httpcommon.cpp
@@ -36,10 +36,6 @@
namespace LLCore
{
-HttpStatus::type_enum_t EXT_CURL_EASY;
-HttpStatus::type_enum_t EXT_CURL_MULTI;
-HttpStatus::type_enum_t LLCORE;
-
HttpStatus::operator U32() const
{
// Effectively, concatenate mType (high) with mStatus (low).
@@ -289,8 +285,10 @@ CURL *getCurlTemplateHandle()
check_curl_code(result, CURLOPT_NOSIGNAL);
result = curl_easy_setopt(curlpTemplateHandle, CURLOPT_NOPROGRESS, 1);
check_curl_code(result, CURLOPT_NOPROGRESS);
+#if LIBCURL_VERSION_MAJOR < 8
result = curl_easy_setopt(curlpTemplateHandle, CURLOPT_ENCODING, "");
check_curl_code(result, CURLOPT_ENCODING);
+#endif
result = curl_easy_setopt(curlpTemplateHandle, CURLOPT_AUTOREFERER, 1);
check_curl_code(result, CURLOPT_AUTOREFERER);
result = curl_easy_setopt(curlpTemplateHandle, CURLOPT_FOLLOWLOCATION, 1);
diff --git a/indra/llcorehttp/httpcommon.h b/indra/llcorehttp/httpcommon.h
index 511a17e000..001fd08f93 100644
--- a/indra/llcorehttp/httpcommon.h
+++ b/indra/llcorehttp/httpcommon.h
@@ -191,7 +191,6 @@
#include "llsd.h"
#include <string>
#include <curl/curl.h>
-#include "boost/noncopyable.hpp"
namespace LLCore
{
@@ -297,25 +296,25 @@ struct HttpStatus
HttpStatus()
{
- mDetails = std::shared_ptr<Details>(new Details(LLCORE, HE_SUCCESS));
+ mDetails = std::make_shared<Details>(LLCORE, HE_SUCCESS);
}
HttpStatus(type_enum_t type, short status)
{
- mDetails = std::shared_ptr<Details>(new Details(type, status));
+ mDetails = std::make_shared<Details>(type, status);
}
HttpStatus(int http_status)
{
- mDetails = std::shared_ptr<Details>(new Details(http_status,
- (http_status >= 200 && http_status <= 299) ? HE_SUCCESS : HE_REPLY_ERROR));
+ mDetails = std::make_shared<Details>(http_status,
+ (http_status >= 200 && http_status <= 299) ? HE_SUCCESS : HE_REPLY_ERROR);
llassert(http_status >= 100 && http_status <= 999);
}
HttpStatus(int http_status, const std::string &message)
{
- mDetails = std::shared_ptr<Details>(new Details(http_status,
- (http_status >= 200 && http_status <= 299) ? HE_SUCCESS : HE_REPLY_ERROR));
+ mDetails = std::make_shared<Details>(http_status,
+ (http_status >= 200 && http_status <= 299) ? HE_SUCCESS : HE_REPLY_ERROR);
llassert(http_status >= 100 && http_status <= 999);
mDetails->mMessage = message;
}
@@ -337,13 +336,13 @@ struct HttpStatus
HttpStatus & clone(const HttpStatus &rhs)
{
- mDetails = std::shared_ptr<Details>(new Details(*rhs.mDetails));
+ mDetails = std::make_shared<Details>(*rhs.mDetails);
return *this;
}
- static const type_enum_t EXT_CURL_EASY = 0; ///< mStatus is an error from a curl_easy_*() call
- static const type_enum_t EXT_CURL_MULTI = 1; ///< mStatus is an error from a curl_multi_*() call
- static const type_enum_t LLCORE = 2; ///< mStatus is an HE_* error code
+ static constexpr type_enum_t EXT_CURL_EASY = 0; ///< mStatus is an error from a curl_easy_*() call
+ static constexpr type_enum_t EXT_CURL_MULTI = 1; ///< mStatus is an error from a curl_multi_*() call
+ static constexpr type_enum_t LLCORE = 2; ///< mStatus is an HE_* error code
///< 100-999 directly represent HTTP status codes
/// Test for successful status in the code regardless
/// of error source (internal, libcurl).
diff --git a/indra/llcorehttp/httpheaders.h b/indra/llcorehttp/httpheaders.h
index a5ca7749b0..6a03cf4083 100644
--- a/indra/llcorehttp/httpheaders.h
+++ b/indra/llcorehttp/httpheaders.h
@@ -74,7 +74,7 @@ namespace LLCore
/// constructor is given a refcount.
///
-class HttpHeaders: private boost::noncopyable
+class HttpHeaders
{
public:
typedef std::pair<std::string, std::string> header_t;
@@ -94,11 +94,11 @@ public:
HttpHeaders();
virtual ~HttpHeaders(); // Use release()
- //typedef LLCoreInt::IntrusivePtr<HttpHeaders> ptr_t;
-protected:
+ // Non-copyable
+ HttpHeaders(const HttpHeaders&) = delete;
+ HttpHeaders& operator=(const HttpHeaders&) = delete;
- HttpHeaders(const HttpHeaders &); // Not defined
- void operator=(const HttpHeaders &); // Not defined
+ //typedef LLCoreInt::IntrusivePtr<HttpHeaders> ptr_t;
public:
// Empty the list of headers.
diff --git a/indra/llcorehttp/httpoptions.cpp b/indra/llcorehttp/httpoptions.cpp
index d85f6039b1..5abd28e211 100644
--- a/indra/llcorehttp/httpoptions.cpp
+++ b/indra/llcorehttp/httpoptions.cpp
@@ -47,6 +47,7 @@ HttpOptions::HttpOptions() :
mVerifyPeer(sDefaultVerifyPeer),
mVerifyHost(false),
mDNSCacheTimeout(-1L),
+ mLastModified(0),
mNoBody(false)
{}
@@ -129,6 +130,11 @@ void HttpOptions::setHeadersOnly(bool nobody)
}
}
+void HttpOptions::setLastModified(time_t lastModified)
+{
+ mLastModified = lastModified;
+}
+
void HttpOptions::setDefaultSSLVerifyPeer(bool verify)
{
sDefaultVerifyPeer = verify;
diff --git a/indra/llcorehttp/httpoptions.h b/indra/llcorehttp/httpoptions.h
index 56a28013cb..d50c5470ae 100644
--- a/indra/llcorehttp/httpoptions.h
+++ b/indra/llcorehttp/httpoptions.h
@@ -55,7 +55,7 @@ namespace LLCore
/// Allocation: Refcounted, heap only. Caller of the constructor
/// is given a refcount.
///
-class HttpOptions : private boost::noncopyable
+class HttpOptions
{
public:
HttpOptions();
@@ -64,10 +64,9 @@ public:
virtual ~HttpOptions(); // Use release()
-protected:
-
- HttpOptions(const HttpOptions &); // Not defined
- void operator=(const HttpOptions &); // Not defined
+ // Non-copyable
+ HttpOptions(const HttpOptions&) = delete;
+ HttpOptions& operator=(const HttpOptions&) = delete;
public:
@@ -178,6 +177,13 @@ public:
return mNoBody;
}
+ // Default: 0
+ void setLastModified(time_t lastModified);
+ time_t getLastModified() const
+ {
+ return mLastModified;
+ }
+
/// Sets default behavior for verifying that the name in the
/// security certificate matches the name of the host contacted.
/// Defaults false if not set, but should be set according to
@@ -199,6 +205,7 @@ protected:
bool mVerifyHost;
int mDNSCacheTimeout;
bool mNoBody;
+ time_t mLastModified;
static bool sDefaultVerifyPeer;
}; // end class HttpOptions
diff --git a/indra/llcorehttp/httprequest.cpp b/indra/llcorehttp/httprequest.cpp
index 2aaf71f8a4..d8fcd6a03f 100644
--- a/indra/llcorehttp/httprequest.cpp
+++ b/indra/llcorehttp/httprequest.cpp
@@ -60,7 +60,7 @@ HttpRequest::HttpRequest()
mRequestQueue = HttpRequestQueue::instanceOf();
mRequestQueue->addRef();
- mReplyQueue.reset( new HttpReplyQueue() );
+ mReplyQueue = std::make_shared<HttpReplyQueue>();
HTTPStats::instance().recordHTTPRequest();
}
@@ -129,7 +129,7 @@ HttpHandle HttpRequest::setPolicyOption(EPolicyOption opt, policy_t pclass,
{
HttpStatus status;
- HttpOpSetGet::ptr_t op(new HttpOpSetGet());
+ HttpOpSetGet::ptr_t op = std::make_shared<HttpOpSetGet>();
if (! (status = op->setupSet(opt, pclass, value)))
{
mLastReqStatus = status;
@@ -152,7 +152,7 @@ HttpHandle HttpRequest::setPolicyOption(EPolicyOption opt, policy_t pclass,
{
HttpStatus status;
- HttpOpSetGet::ptr_t op (new HttpOpSetGet());
+ HttpOpSetGet::ptr_t op = std::make_shared<HttpOpSetGet>();
if (! (status = op->setupSet(opt, pclass, value)))
{
mLastReqStatus = status;
@@ -190,7 +190,7 @@ HttpHandle HttpRequest::requestGet(policy_t policy_id,
LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
HttpStatus status;
- HttpOpRequest::ptr_t op(new HttpOpRequest());
+ HttpOpRequest::ptr_t op = std::make_shared<HttpOpRequest>();
if (! (status = op->setupGet(policy_id, url, options, headers)))
{
mLastReqStatus = status;
@@ -219,7 +219,7 @@ HttpHandle HttpRequest::requestGetByteRange(policy_t policy_id,
LL_PROFILE_ZONE_SCOPED_CATEGORY_NETWORK;
HttpStatus status;
- HttpOpRequest::ptr_t op(new HttpOpRequest());
+ HttpOpRequest::ptr_t op = std::make_shared<HttpOpRequest>();
if (! (status = op->setupGetByteRange(policy_id, url, offset, len, options, headers)))
{
mLastReqStatus = status;
@@ -246,7 +246,7 @@ HttpHandle HttpRequest::requestPost(policy_t policy_id,
{
HttpStatus status;
- HttpOpRequest::ptr_t op(new HttpOpRequest());
+ HttpOpRequest::ptr_t op = std::make_shared<HttpOpRequest>();
if (! (status = op->setupPost(policy_id, url, body, options, headers)))
{
mLastReqStatus = status;
@@ -273,7 +273,7 @@ HttpHandle HttpRequest::requestPut(policy_t policy_id,
{
HttpStatus status;
- HttpOpRequest::ptr_t op (new HttpOpRequest());
+ HttpOpRequest::ptr_t op = std::make_shared<HttpOpRequest>();
if (! (status = op->setupPut(policy_id, url, body, options, headers)))
{
mLastReqStatus = status;
@@ -298,7 +298,7 @@ HttpHandle HttpRequest::requestDelete(policy_t policy_id,
{
HttpStatus status;
- HttpOpRequest::ptr_t op(new HttpOpRequest());
+ HttpOpRequest::ptr_t op = std::make_shared<HttpOpRequest>();
if (!(status = op->setupDelete(policy_id, url, options, headers)))
{
mLastReqStatus = status;
@@ -324,7 +324,7 @@ HttpHandle HttpRequest::requestPatch(policy_t policy_id,
{
HttpStatus status;
- HttpOpRequest::ptr_t op (new HttpOpRequest());
+ HttpOpRequest::ptr_t op = std::make_shared<HttpOpRequest>();
if (!(status = op->setupPatch(policy_id, url, body, options, headers)))
{
mLastReqStatus = status;
@@ -349,7 +349,7 @@ HttpHandle HttpRequest::requestCopy(policy_t policy_id,
{
HttpStatus status;
- HttpOpRequest::ptr_t op(new HttpOpRequest());
+ HttpOpRequest::ptr_t op = std::make_shared<HttpOpRequest>();
if (!(status = op->setupCopy(policy_id, url, options, headers)))
{
mLastReqStatus = status;
@@ -375,7 +375,7 @@ HttpHandle HttpRequest::requestMove(policy_t policy_id,
{
HttpStatus status;
- HttpOpRequest::ptr_t op (new HttpOpRequest());
+ HttpOpRequest::ptr_t op = std::make_shared<HttpOpRequest>();
if (!(status = op->setupMove(policy_id, url, options, headers)))
{
mLastReqStatus = status;
@@ -397,7 +397,7 @@ HttpHandle HttpRequest::requestNoOp(HttpHandler::ptr_t user_handler)
{
HttpStatus status;
- HttpOperation::ptr_t op (new HttpOpNull());
+ HttpOperation::ptr_t op = std::make_shared<HttpOpNull>();
op->setReplyPath(mReplyQueue, user_handler);
if (! (status = mRequestQueue->addOp(op))) // transfers refcount
{
@@ -463,7 +463,7 @@ HttpHandle HttpRequest::requestCancel(HttpHandle request, HttpHandler::ptr_t use
{
HttpStatus status;
- HttpOperation::ptr_t op(new HttpOpCancel(request));
+ HttpOperation::ptr_t op = std::make_shared<HttpOpCancel>(request);
op->setReplyPath(mReplyQueue, user_handler);
if (! (status = mRequestQueue->addOp(op))) // transfers refcount
{
@@ -528,7 +528,7 @@ HttpHandle HttpRequest::requestStopThread(HttpHandler::ptr_t user_handler)
HttpStatus status;
HttpHandle handle(LLCORE_HTTP_HANDLE_INVALID);
- HttpOperation::ptr_t op(new HttpOpStop());
+ HttpOperation::ptr_t op = std::make_shared<HttpOpStop>();
op->setReplyPath(mReplyQueue, user_handler);
if (! (status = mRequestQueue->addOp(op))) // transfers refcount
{
@@ -548,7 +548,7 @@ HttpHandle HttpRequest::requestSpin(int mode)
HttpStatus status;
HttpHandle handle(LLCORE_HTTP_HANDLE_INVALID);
- HttpOperation::ptr_t op(new HttpOpSpin(mode));
+ HttpOperation::ptr_t op = std::make_shared<HttpOpSpin>(mode);
op->setReplyPath(mReplyQueue, HttpHandler::ptr_t());
if (! (status = mRequestQueue->addOp(op))) // transfers refcount
{
diff --git a/indra/llcorehttp/httprequest.h b/indra/llcorehttp/httprequest.h
index e6e051410e..f7516f127a 100644
--- a/indra/llcorehttp/httprequest.h
+++ b/indra/llcorehttp/httprequest.h
@@ -104,9 +104,9 @@ public:
/// Represents a default, catch-all policy class that guarantees
/// eventual service for any HTTP request.
- static const policy_t DEFAULT_POLICY_ID = 0;
- static const policy_t INVALID_POLICY_ID = 0xFFFFFFFFU;
- static const policy_t GLOBAL_POLICY_ID = 0xFFFFFFFEU;
+ static constexpr policy_t DEFAULT_POLICY_ID = 0;
+ static constexpr policy_t INVALID_POLICY_ID = 0xFFFFFFFFU;
+ static constexpr policy_t GLOBAL_POLICY_ID = 0xFFFFFFFEU;
/// Create a new policy class into which requests can be made.
///
@@ -237,7 +237,7 @@ public:
/// Prototype for policy based callbacks. The callback methods will be executed
/// on the worker thread so no modifications should be made to the HttpHandler object.
- typedef boost::function<HttpStatus(const std::string &, const HttpHandler::ptr_t &, void *)> policyCallback_t;
+ typedef std::function<HttpStatus(const std::string &, const HttpHandler::ptr_t &, void *)> policyCallback_t;
/// Set a policy option for a global or class parameter at
/// startup time (prior to thread start).
diff --git a/indra/llcorehttp/httpresponse.h b/indra/llcorehttp/httpresponse.h
index 99c8f1d2f9..ed2d76c10f 100644
--- a/indra/llcorehttp/httpresponse.h
+++ b/indra/llcorehttp/httpresponse.h
@@ -65,8 +65,8 @@ public:
protected:
virtual ~HttpResponse(); // Use release()
- HttpResponse(const HttpResponse &); // Not defined
- void operator=(const HttpResponse &); // Not defined
+ HttpResponse(const HttpResponse&) = delete;
+ void operator=(const HttpResponse&) = delete;
public:
/// Statistics for the HTTP
diff --git a/indra/llcorehttp/httpstats.cpp b/indra/llcorehttp/httpstats.cpp
index dc2ac35c71..f64752e91e 100644
--- a/indra/llcorehttp/httpstats.cpp
+++ b/indra/llcorehttp/httpstats.cpp
@@ -41,6 +41,7 @@ HTTPStats::~HTTPStats()
void HTTPStats::resetStats()
{
+ std::lock_guard<std::mutex> lock(mStatsMutex);
mResutCodes.clear();
mDataDown.reset();
mDataUp.reset();
@@ -50,6 +51,7 @@ void HTTPStats::resetStats()
void HTTPStats::recordResultCode(S32 code)
{
+ std::lock_guard<std::mutex> lock(mStatsMutex);
std::map<S32, S32>::iterator it;
it = mResutCodes.find(code);
@@ -86,6 +88,7 @@ namespace
void HTTPStats::dumpStats()
{
+ std::lock_guard<std::mutex> lock(mStatsMutex);
std::stringstream out;
out << "HTTP DATA SUMMARY" << std::endl;
diff --git a/indra/llcorehttp/httpstats.h b/indra/llcorehttp/httpstats.h
index 5c0f26d34e..08a537cda7 100644
--- a/indra/llcorehttp/httpstats.h
+++ b/indra/llcorehttp/httpstats.h
@@ -47,17 +47,23 @@ namespace LLCore
void recordDataDown(size_t bytes)
{
+ std::lock_guard<std::mutex> lock(mStatsMutex);
mDataDown.push((F32)bytes);
}
void recordDataUp(size_t bytes)
{
+ std::lock_guard<std::mutex> lock(mStatsMutex);
mDataUp.push((F32)bytes);
}
- void recordHTTPRequest() { ++mRequests; }
+ void recordHTTPRequest()
+ {
+ std::lock_guard<std::mutex> lock(mStatsMutex);
+ ++mRequests;
+ }
- void recordResultCode(S32 code);
+ void recordResultCode(S32 code); // http thread
void dumpStats();
private:
@@ -67,6 +73,8 @@ namespace LLCore
S32 mRequests;
std::map<S32, S32> mResutCodes;
+
+ std::mutex mStatsMutex;
};
diff --git a/indra/llcorehttp/llhttpconstants.cpp b/indra/llcorehttp/llhttpconstants.cpp
index 40d6c7506c..7a671543d9 100755..100644
--- a/indra/llcorehttp/llhttpconstants.cpp
+++ b/indra/llcorehttp/llhttpconstants.cpp
@@ -100,6 +100,7 @@ const std::string HTTP_IN_HEADER_LOCATION("location");
const std::string HTTP_IN_HEADER_RETRY_AFTER("retry-after");
const std::string HTTP_IN_HEADER_SET_COOKIE("set-cookie");
const std::string HTTP_IN_HEADER_USER_AGENT("user-agent");
+const std::string HTTP_IN_HEADER_X_CONTENT_TYPE_OPTIONS("x-content-type-options");
const std::string HTTP_IN_HEADER_X_FORWARDED_FOR("x-forwarded-for");
const std::string HTTP_CONTENT_LLSD_XML("application/llsd+xml");
@@ -122,6 +123,7 @@ const std::string HTTP_CONTENT_IMAGE_BMP("image/bmp");
const std::string HTTP_NO_CACHE("no-cache");
const std::string HTTP_NO_CACHE_CONTROL("no-cache, max-age=0");
+const std::string HTTP_NOSNIFF("nosniff");
const std::string HTTP_VERB_INVALID("(invalid)");
const std::string HTTP_VERB_HEAD("HEAD");
diff --git a/indra/llcorehttp/llhttpconstants.h b/indra/llcorehttp/llhttpconstants.h
index 583f9fbcb7..d05d0f1af1 100755..100644
--- a/indra/llcorehttp/llhttpconstants.h
+++ b/indra/llcorehttp/llhttpconstants.h
@@ -100,6 +100,8 @@ extern const std::string HTTP_VERB_POST;
extern const std::string HTTP_VERB_DELETE;
extern const std::string HTTP_VERB_MOVE;
extern const std::string HTTP_VERB_OPTIONS;
+extern const std::string HTTP_VERB_PATCH;
+extern const std::string HTTP_VERB_COPY;
enum EHTTPMethod
{
@@ -190,6 +192,7 @@ extern const std::string HTTP_IN_HEADER_LOCATION;
extern const std::string HTTP_IN_HEADER_RETRY_AFTER;
extern const std::string HTTP_IN_HEADER_SET_COOKIE;
extern const std::string HTTP_IN_HEADER_USER_AGENT;
+extern const std::string HTTP_IN_HEADER_X_CONTENT_TYPE_OPTIONS;
extern const std::string HTTP_IN_HEADER_X_FORWARDED_FOR;
//// HTTP Content Types ////
@@ -215,5 +218,6 @@ extern const std::string HTTP_CONTENT_IMAGE_BMP;
//// HTTP Cache Settings ////
extern const std::string HTTP_NO_CACHE;
extern const std::string HTTP_NO_CACHE_CONTROL;
+extern const std::string HTTP_NOSNIFF;
#endif
diff --git a/indra/llcorehttp/lljsonrpcws.cpp b/indra/llcorehttp/lljsonrpcws.cpp
new file mode 100644
index 0000000000..f04738d805
--- /dev/null
+++ b/indra/llcorehttp/lljsonrpcws.cpp
@@ -0,0 +1,902 @@
+/**
+ * @file lljsonrpcws.cpp
+ * @brief JSON-RPC 2.0 WebSocket server and connection implementation
+ *
+ * $LicenseInfo:firstyear=2025&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2025, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ */
+
+#include "linden_common.h"
+
+#include "lljsonrpcws.h"
+#include "llerror.h"
+#include "llsdjson.h"
+#include "lldate.h"
+#include "llcoros.h"
+#include "lltimer.h"
+#include "workqueue.h"
+
+#include <boost/json.hpp>
+
+//========================================================================
+// LLJSONRPCConnection Implementation
+//========================================================================
+
+void LLJSONRPCConnection::onOpen()
+{
+ LL_INFOS("JSONRPC") << "JSON-RPC connection opened" << LL_ENDL;
+}
+
+void LLJSONRPCConnection::onClose()
+{
+ // Move the pending-request map out under the lock so we can invoke the
+ // callbacks without holding it (callbacks may themselves call into this
+ // connection).
+ std::unordered_map<std::string, ResponseCallback> pending;
+ {
+ LLMutexLock lock(&mMutex);
+ pending.swap(mPendingRequests);
+ // Deadlines correspond to entries in mPendingRequests; drop them.
+ std::priority_queue<PendingDeadline> empty;
+ mPendingDeadlines.swap(empty);
+ }
+
+ LL_INFOS("JSONRPC") << "JSON-RPC connection closed, clearing "
+ << pending.size() << " pending requests" << LL_ENDL;
+
+ for (auto& [id, callback] : pending)
+ {
+ if (callback)
+ {
+ LLSD error;
+ error["code"] = RPCError::CONNECTION_CLOSED;
+ error["message"] = "Connection closed";
+ callback(LLSD(), error);
+ }
+ }
+}
+
+void LLJSONRPCConnection::onMessage(const std::string& message)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET;
+ LL_DEBUGS("JSONRPC") << "Received JSON-RPC message: " << message << LL_ENDL;
+
+ try
+ {
+ // Parse JSON message
+ boost::system::error_code ec;
+ boost::json::value json_value = boost::json::parse(message, ec);
+
+ if (ec.failed())
+ {
+ LL_WARNS("JSONRPC") << "Failed to parse JSON: " << ec.message() << LL_ENDL;
+ sendError(LLSD(), ParseError(ec.message()));
+ return;
+ }
+
+ // Convert to LLSD
+ LLSD message_obj = LlsdFromJson(json_value);
+
+ // Handle batch vs single message
+ if (message_obj.isArray())
+ {
+ // JSON-RPC 2.0 batch requests are intentionally not supported.
+ // No known client (including the sl-vscode-plugin) sends batches,
+ // and a spec-compliant implementation would require accumulating
+ // responses across sync + async handlers before shipping a single
+ // array frame. If a real use case appears, implement per
+ // JSON-RPC 2.0 §6.
+ sendError(LLSD(), InvalidRequest("Batch requests are not supported"));
+ return;
+ }
+
+ // Single message
+ processMessage(message_obj);
+ }
+ catch (const std::exception& e)
+ {
+ LL_WARNS("JSONRPC") << "Exception processing JSON-RPC message: " << e.what() << LL_ENDL;
+ sendError(LLSD(), InternalError());
+ }
+}
+
+void LLJSONRPCConnection::processMessage(const LLSD& message_obj)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET;
+ try
+ {
+ // Determine if this is a request, notification, or response
+ if (message_obj.has("method"))
+ {
+ // This is a request or notification
+ if (validateMessage(message_obj, true))
+ {
+ processRequest(message_obj);
+ }
+ }
+ else if (message_obj.has("result") || message_obj.has("error"))
+ {
+ // This is a response
+ if (validateMessage(message_obj, false))
+ {
+ processResponse(message_obj);
+ }
+ }
+ else
+ {
+ LL_WARNS("JSONRPC") << "Message must contain 'method' or 'result'/'error'" << LL_ENDL;
+ }
+ }
+ catch (const RPCError& e)
+ {
+ LLSD id = message_obj.has("id") ? message_obj["id"] : LLSD();
+ sendError(id, e);
+ }
+}
+
+void LLJSONRPCConnection::processRequest(const LLSD& request)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET;
+ std::string method = request["method"].asString();
+ LLSD params = request.has("params") ? request["params"] : LLSD();
+ LLSD id = request.has("id") ? request["id"] : LLSD();
+ bool is_notification = !request.has("id");
+
+ LL_DEBUGS("JSONRPC") << "Processing " << (is_notification ? "notification" : "request")
+ << " for method: " << method << LL_ENDL;
+
+ // Nothing for a peer that has not proven itself, and nothing said of
+ // which methods there are.
+ if (!mAuthenticated)
+ {
+ LL_DEBUGS("JSONRPC") << "Refused " << method << " from a peer not authenticated" << LL_ENDL;
+ if (!is_notification)
+ {
+ sendError(id, UnauthorizedError());
+ }
+ return;
+ }
+
+ // Resolve the handler under the mutex, then invoke it unlocked.
+ MethodHandler handler;
+ bool is_async = false;
+ {
+ LLMutexLock lock(&mMutex);
+ auto async_it = mAsyncMethodHandlers.find(method);
+ if (async_it != mAsyncMethodHandlers.end())
+ {
+ handler = async_it->second;
+ is_async = true;
+ }
+ else
+ {
+ auto sync_it = mMethodHandlers.find(method);
+ if (sync_it != mMethodHandlers.end())
+ {
+ handler = sync_it->second;
+ }
+ }
+ }
+
+ if (is_async)
+ {
+ // Async handler — launched as a coroutine, response sent by the lambda.
+ if (is_notification)
+ {
+ LL_WARNS("JSONRPC") << "Method " << method
+ << " called as notification; rejecting request" << LL_ENDL;
+ throw InvalidRequest("Method " + method + " cannot be called as a notification");
+ }
+ ptr_t conn = std::static_pointer_cast<LLJSONRPCConnection>(getSelfPtr());
+ if (!conn)
+ {
+ LL_WARNS("JSONRPC") << "Connection expired before method " << method
+ << " could be launched; failing request" << LL_ENDL;
+ throw InternalError("Connection expired before method " + method
+ + " could be launched");
+ }
+ bool posted = LL::WorkQueue::postToMainLoop(
+ [handler, method, id, params, conn]()
+ {
+ LLCoros::instance().launch(
+ "JSONRPC::" + method,
+ [handler, method, id, params, conn]()
+ {
+ try
+ {
+ LLSD result = handler(method, id, params);
+ if (conn->isConnected())
+ {
+ conn->sendResponse(id, result);
+ }
+ else
+ {
+ LL_WARNS("JSONRPC") << "Connection closed before async method "
+ << method << " could send response" << LL_ENDL;
+ }
+ }
+ catch (const RPCError& e)
+ {
+ if (conn->isConnected())
+ {
+ conn->sendError(id, e);
+ }
+ else
+ {
+ LL_WARNS("JSONRPC") << "Connection closed before async method "
+ << method << " could send error" << LL_ENDL;
+ }
+ }
+ catch (const std::exception& e)
+ {
+ if (conn->isConnected())
+ {
+ LL_WARNS("JSONRPC") << "Exception in async method " << method
+ << ": " << e.what() << LL_ENDL;
+ conn->sendError(id, InternalError());
+ }
+ else
+ {
+ LL_WARNS("JSONRPC") << "Connection closed before async method "
+ << method << " could send error" << LL_ENDL;
+ }
+ }
+ });
+ });
+ if (!posted)
+ {
+ LL_WARNS("JSONRPC") << "Main loop unavailable; failing async method " << method << LL_ENDL;
+ throw InternalError("Viewer is shutting down; cannot service " + method);
+ }
+ return;
+ }
+
+ if (!handler)
+ {
+ if (!is_notification)
+ {
+ sendError(id, MethodNotFound(method));
+ }
+ return;
+ }
+
+ try
+ {
+ LLSD result = handler(method, id, params);
+
+ if (!is_notification)
+ {
+ sendResponse(id, result);
+ }
+ }
+ catch (const RPCError& e)
+ {
+ if (!is_notification)
+ {
+ sendError(id, e);
+ }
+ else
+ {
+ LL_WARNS("JSONRPC") << "Error in notification handler for " << method
+ << ": " << e.what() << LL_ENDL;
+ }
+ }
+ catch (const std::exception& e)
+ {
+ if (!is_notification)
+ {
+ LL_WARNS("JSONRPC") << "Exception in handler for " << method
+ << ": " << e.what() << LL_ENDL;
+ sendError(id, InternalError());
+ }
+ else
+ {
+ LL_WARNS("JSONRPC") << "Exception in notification handler for " << method
+ << ": " << e.what() << LL_ENDL;
+ }
+ }
+}
+
+void LLJSONRPCConnection::processResponse(const LLSD& response)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET;
+ if (!response.has("id"))
+ {
+ LL_WARNS("JSONRPC") << "Response missing id field" << LL_ENDL;
+ return;
+ }
+
+ std::string id = response["id"].asString();
+ ResponseCallback callback;
+ {
+ LLMutexLock lock(&mMutex);
+ auto it = mPendingRequests.find(id);
+ if (it == mPendingRequests.end())
+ {
+ LL_WARNS("JSONRPC") << "Received response for unknown request id: " << id << LL_ENDL;
+ return;
+ }
+ callback = std::move(it->second);
+ mPendingRequests.erase(it);
+ }
+
+ if (callback)
+ {
+ LLSD result = response.has("result") ? response["result"] : LLSD();
+ LLSD error = response.has("error") ? response["error"] : LLSD();
+
+ callback(result, error);
+ }
+}
+
+bool LLJSONRPCConnection::validateMessage(const LLSD& message, bool is_request)
+{
+ // Check JSON-RPC version
+ if (!message.has("jsonrpc") || message["jsonrpc"].asString() != "2.0")
+ {
+ LL_WARNS("JSONRPC") << "Missing or invalid jsonrpc version" << LL_ENDL;
+ return false;
+ }
+
+ if (is_request)
+ {
+ // Request/notification validation
+ if (!message.has("method"))
+ {
+ LL_WARNS("JSONRPC") << "Missing method field" << LL_ENDL;
+ return false;
+ }
+
+ if (!message["method"].isString())
+ {
+ LL_WARNS("JSONRPC") << "Method must be a string" << LL_ENDL;
+ return false;
+ }
+
+ // Params are optional but must be array or object if present
+ if (message.has("params"))
+ {
+ if (!message["params"].isArray() && !message["params"].isMap())
+ {
+ LL_WARNS("JSONRPC") << "Params must be array or object" << LL_ENDL;
+ return false;
+ }
+ }
+ }
+ else
+ {
+ // Response validation
+ if (!message.has("id"))
+ {
+ LL_WARNS("JSONRPC") << "Response missing id field" << LL_ENDL;
+ return false;
+ }
+
+ // Must have either result or error, but not both
+ bool has_result = message.has("result");
+ bool has_error = message.has("error");
+
+ if (!has_result && !has_error)
+ {
+ LL_WARNS("JSONRPC") << "Response must have result or error" << LL_ENDL;
+ return false;
+ }
+
+ if (has_result && has_error)
+ {
+ LL_WARNS("JSONRPC") << "Response cannot have both result and error" << LL_ENDL;
+ return false;
+ }
+
+ // Error must be an object with code and message
+ if (has_error)
+ {
+ LLSD error = message["error"];
+ if (!error.isMap())
+ {
+ LL_WARNS("JSONRPC") << "Error must be an object" << LL_ENDL;
+ return false;
+ }
+ if (!error.has("code") || !error.has("message"))
+ {
+ LL_WARNS("JSONRPC") << "Error must have code and message" << LL_ENDL;
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+void LLJSONRPCConnection::sweepTimeouts()
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET;
+ // Pop expired deadlines and collect their callbacks. Tombstones (entries
+ // whose request already completed) are silently discarded.
+ std::vector<std::pair<std::string, ResponseCallback>> expired;
+ const F64 now = LLTimer::getTotalSeconds();
+ {
+ LLMutexLock lock(&mMutex);
+ while (!mPendingDeadlines.empty() && mPendingDeadlines.top().mDeadline <= now)
+ {
+ std::string id = mPendingDeadlines.top().mId;
+ mPendingDeadlines.pop();
+ auto it = mPendingRequests.find(id);
+ if (it != mPendingRequests.end())
+ {
+ expired.emplace_back(std::move(id), std::move(it->second));
+ mPendingRequests.erase(it);
+ }
+ }
+ }
+
+ for (auto& [id, callback] : expired)
+ {
+ LL_WARNS("JSONRPC") << "Request " << id << " timed out" << LL_ENDL;
+ if (callback)
+ {
+ LLSD error;
+ error["code"] = RPCError::REQUEST_TIMEOUT;
+ error["message"] = "Request timed out";
+ callback(LLSD(), error);
+ }
+ }
+}
+
+void LLJSONRPCConnection::testInjectPendingRequest(const std::string& id, F64 deadline, ResponseCallback callback)
+{
+ LLMutexLock lock(&mMutex);
+ mPendingRequests[id] = std::move(callback);
+ mPendingDeadlines.push({ deadline, id });
+}
+
+void LLJSONRPCConnection::testSweepTimeouts()
+{
+ sweepTimeouts();
+}
+
+size_t LLJSONRPCConnection::testPendingRequestCount() const
+{
+ LLMutexLock lock(&mMutex);
+ return mPendingRequests.size();
+}
+
+LLSD LLJSONRPCConnection::generateId()
+{
+ // Server-wide atomic counter for efficient unique ID generation.
+ // Start above zero to avoid conflicts with any manual test IDs.
+ static constexpr U64 REQUEST_ID_START = 1000;
+ static std::atomic<U64> sRequestIdCounter{REQUEST_ID_START};
+
+ // Generate server-unique sequential ID
+ U64 id = sRequestIdCounter.fetch_add(1);
+ return LLSD(llformat("rpc_%llu", id));
+}
+
+void LLJSONRPCConnection::registerMethod(const std::string& method, MethodHandler handler)
+{
+ {
+ LLMutexLock lock(&mMutex);
+ mMethodHandlers[method] = std::move(handler);
+ }
+ LL_DEBUGS("JSONRPC") << "Registered method: " << method << LL_ENDL;
+}
+
+void LLJSONRPCConnection::registerAsyncMethod(const std::string& method, MethodHandler handler)
+{
+ {
+ LLMutexLock lock(&mMutex);
+ mAsyncMethodHandlers[method] = std::move(handler);
+ }
+ LL_DEBUGS("JSONRPC") << "Registered async method: " << method << LL_ENDL;
+}
+
+void LLJSONRPCConnection::unregisterMethod(const std::string& method)
+{
+ {
+ LLMutexLock lock(&mMutex);
+ mMethodHandlers.erase(method);
+ mAsyncMethodHandlers.erase(method);
+ }
+ LL_DEBUGS("JSONRPC") << "Unregistered method: " << method << LL_ENDL;
+}
+
+std::set<std::string> LLJSONRPCConnection::getMethods() const
+{
+ LLMutexLock lock(&mMutex);
+ std::set<std::string> methods;
+
+ for (const auto& [method, handler] : mMethodHandlers)
+ {
+ methods.insert(method);
+ }
+
+ for (const auto& [method, handler] : mAsyncMethodHandlers)
+ {
+ methods.insert(method);
+ }
+
+ return methods;
+}
+
+LLSD LLJSONRPCConnection::makeEnvelope(const LLSD& id,
+ const std::string& method,
+ const LLSD& params,
+ const LLSD& result,
+ const LLSD& error)
+{
+ LLSD env;
+ env["jsonrpc"] = "2.0";
+ // Notifications (requests without an id) are the only case that omits id.
+ if (!(id.isUndefined() && !method.empty()))
+ {
+ env["id"] = id;
+ }
+ if (!method.empty())
+ {
+ env["method"] = method;
+ }
+ if (params.isDefined())
+ {
+ env["params"] = params;
+ }
+ if (result.isDefined())
+ {
+ env["result"] = result;
+ }
+ if (error.isDefined())
+ {
+ env["error"] = error;
+ }
+ return env;
+}
+
+LLSD LLJSONRPCConnection::call(const std::string& method, const LLSD& params, ResponseCallback callback, F64 timeout)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET;
+ LLSD id = generateId();
+ LLSD request = makeEnvelope(id, method, params, LLSD(), LLSD());
+ const std::string id_str = id.asString();
+
+ // Store callback if provided. Fire-and-forget calls (no callback) are
+ // not tracked for timeouts since there is nobody to deliver the error to.
+ if (callback)
+ {
+ LLMutexLock lock(&mMutex);
+ mPendingRequests[id_str] = std::move(callback);
+ mPendingDeadlines.push({ LLTimer::getTotalSeconds() + timeout, id_str });
+ }
+
+ // Send the request
+ if (!sendMessage(LlsdToJson(request)))
+ {
+ // Remove from pending if send failed
+ {
+ LLMutexLock lock(&mMutex);
+ mPendingRequests.erase(id_str);
+ }
+ LL_WARNS("JSONRPC") << "Failed to send request" << LL_ENDL;
+ return LLSD();
+ }
+
+ LL_DEBUGS("JSONRPC") << "Sent request: " << method << " with id: " << id.asString() << LL_ENDL;
+ return id;
+}
+
+bool LLJSONRPCConnection::notify(const std::string& method, const LLSD& params)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET;
+ LLSD notification = makeEnvelope(LLSD(), method, params, LLSD(), LLSD());
+
+ if (!sendMessage(LlsdToJson(notification)))
+ {
+ LL_WARNS("JSONRPC") << "Failed to send notification" << LL_ENDL;
+ return false;
+ }
+
+ LL_DEBUGS("JSONRPC") << "Sent notification: " << method << LL_ENDL;
+ return true;
+}
+
+bool LLJSONRPCConnection::sendResponse(const LLSD& id, const LLSD& result)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET;
+ LLSD response = makeEnvelope(id, std::string(), LLSD(), result, LLSD());
+
+ if (!sendMessage(LlsdToJson(response)))
+ {
+ LL_WARNS("JSONRPC") << "Failed to send response for id: " << id.asString() << LL_ENDL;
+ return false;
+ }
+ LL_DEBUGS("JSONRPC") << "Sent response for id: " << id.asString() << LL_ENDL;
+ return true;
+}
+
+bool LLJSONRPCConnection::sendError(const LLSD& id, const RPCError& error)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET;
+ LLSD error_obj;
+ error_obj["code"] = error.getCode();
+ error_obj["message"] = error.what();
+
+ if (!error.getData().isUndefined())
+ {
+ error_obj["data"] = error.getData();
+ }
+
+ // Responses always include id; an undefined id serializes as null (used for parse errors).
+ LLSD response = makeEnvelope(id, std::string(), LLSD(), LLSD(), error_obj);
+
+ if (!sendMessage(LlsdToJson(response)))
+ {
+ LL_WARNS("JSONRPC") << "Failed to send error response" << LL_ENDL;
+ return false;
+ }
+ LL_DEBUGS("JSONRPC") << "Sent error response: " << error.what() << LL_ENDL;
+ return true;
+}
+
+bool LLJSONRPCConnection::sendBatch(const LLSD& batch, ResponseCallback callback)
+{
+ if (!batch.isArray() || batch.size() == 0)
+ {
+ LL_WARNS("JSONRPC") << "Batch must be non-empty array" << LL_ENDL;
+ return false;
+ }
+
+ // For batch requests with callbacks, we need to track multiple responses
+ // This is complex as we need to correlate all responses before calling callback
+ // For now, we'll send the batch but won't support batch response callbacks
+ if (callback)
+ {
+ LL_WARNS("JSONRPC") << "Batch response callbacks not yet implemented" << LL_ENDL;
+ }
+
+ if (!sendMessage(LlsdToJson(batch)))
+ {
+ LL_WARNS("JSONRPC") << "Failed to send batch" << LL_ENDL;
+ return false;
+ }
+
+ LL_DEBUGS("JSONRPC") << "Sent batch with " << batch.size() << " messages" << LL_ENDL;
+ return true;
+}
+
+//========================================================================
+// LLJSONRPCServer Implementation
+//========================================================================
+
+LLJSONRPCServer::LLJSONRPCServer(const std::string& name, U16 port, bool local_only)
+ : LLWebsocketMgr::WSServer(name, port, local_only), mServerName(name)
+{
+ LL_INFOS("JSONRPC") << "Created JSON-RPC server: " << name
+ << " on port " << port << LL_ENDL;
+
+ // Register standard JSON-RPC methods
+ registerGlobalMethod("system.getStats", [this](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD {
+ LL_DEBUGS("JSONRPC") << "System method " << method << " called" << LL_ENDL;
+ return getServerStats();
+ });
+
+}
+
+LLWebsocketMgr::WSConnection::ptr_t LLJSONRPCServer::connectionFactory(LLWebsocketMgr::WSServer::ptr_t server,
+ LLWebsocketMgr::connection_h handle)
+{
+ auto connection = std::make_shared<LLJSONRPCConnection>(server, handle);
+ setupConnectionMethods(connection);
+ return connection;
+}
+
+void LLJSONRPCServer::onConnectionOpened(const LLWebsocketMgr::WSConnection::ptr_t& connection)
+{
+ LL_INFOS("JSONRPC") << "JSON-RPC client connected, total connections: "
+ << getConnectionCount() << LL_ENDL;
+}
+
+void LLJSONRPCServer::onConnectionClosed(const LLWebsocketMgr::WSConnection::ptr_t& connection)
+{
+ LL_INFOS("JSONRPC") << "JSON-RPC client disconnected, total connections: "
+ << getConnectionCount() << LL_ENDL;
+}
+
+bool LLJSONRPCServer::update()
+{
+ // Main thread, once per frame; throttled to TIMEOUT_SWEEP_INTERVAL.
+ if (mSweepTimer.getElapsedTimeF32() < LLJSONRPCConnection::TIMEOUT_SWEEP_INTERVAL)
+ {
+ return true;
+ }
+ mSweepTimer.reset();
+
+ for (const auto& conn : getConnections())
+ {
+ if (auto rpc = std::dynamic_pointer_cast<LLJSONRPCConnection>(conn))
+ {
+ rpc->sweepTimeouts();
+ }
+ }
+ return true;
+}
+
+void LLJSONRPCServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t connection)
+{
+ LLMutexLock lock(&mGlobalMethodsMutex);
+
+ // Register all global methods on the new connection
+ for (const auto& [method, handler] : mGlobalMethods)
+ {
+ connection->registerMethod(method, handler);
+ }
+
+ std::weak_ptr<LLJSONRPCConnection> weak_connection = connection;
+ connection->registerMethod(
+ "system.listMethods",
+ [weak_connection](
+ const std::string&,
+ const LLSD&,
+ const LLSD&) -> LLSD
+ {
+ LLSD methods(LLSD::emptyArray());
+ auto connection = weak_connection.lock();
+ if (!connection)
+ {
+ return methods;
+ }
+
+ for (const std::string& method : connection->getMethods())
+ {
+ methods.append(method);
+ }
+
+ return methods;
+ });
+
+ connection->registerMethod(
+ "system.ping",
+ [this, weak_connection](
+ const std::string& method,
+ const LLSD& id,
+ const LLSD& params) -> LLSD
+ {
+ LL_DEBUGS("JSONRPC") << "System method " << method
+ << " called" << LL_ENDL;
+ return handlePing(weak_connection.lock(), params);
+ });
+
+ connection->registerMethod(
+ "system.getVersion",
+ [this, weak_connection](
+ const std::string& method,
+ const LLSD& id,
+ const LLSD& params) -> LLSD
+ {
+ LL_DEBUGS("JSONRPC") << "System method " << method
+ << " called" << LL_ENDL;
+ return handleGetVersion(weak_connection.lock(), params);
+ });
+
+ connection->registerMethod(
+ "system.status",
+ [this, weak_connection](
+ const std::string& method,
+ const LLSD& id,
+ const LLSD& params) -> LLSD
+ {
+ LL_DEBUGS("JSONRPC") << "System method " << method
+ << " called" << LL_ENDL;
+ return handleStatus(weak_connection.lock(), params);
+ });
+}
+
+LLSD LLJSONRPCServer::handlePing(
+ const LLJSONRPCConnection::ptr_t& connection,
+ const LLSD& params) const
+{
+ return LLSD("pong");
+}
+
+LLSD LLJSONRPCServer::handleStatus(
+ const LLJSONRPCConnection::ptr_t& connection,
+ const LLSD& params) const
+{
+ LLSD result;
+ result["status"] = "OK";
+ return result;
+}
+
+void LLJSONRPCServer::registerGlobalMethod(const std::string& method, MethodHandler handler)
+{
+ {
+ LLMutexLock lock(&mGlobalMethodsMutex);
+ mGlobalMethods[method] = handler;
+ }
+
+ // Apply to all existing connections - we need to iterate through connections
+ // Since mConnections is private, we need to use broadcastMessage or find another approach
+ // For now, we'll only apply to new connections
+
+ LL_INFOS("JSONRPC") << "Registered global method: " << method << LL_ENDL;
+}
+
+void LLJSONRPCServer::unregisterGlobalMethod(const std::string& method)
+{
+ {
+ LLMutexLock lock(&mGlobalMethodsMutex);
+ mGlobalMethods.erase(method);
+ }
+
+ // For existing connections, we would need access to them
+ // This is a limitation of the current design - methods added after connection
+ // establishment won't be retroactively applied
+
+ LL_INFOS("JSONRPC") << "Unregistered global method: " << method << LL_ENDL;
+}
+
+LLSD LLJSONRPCServer::getMethodList() const
+{
+ LLMutexLock lock(&mGlobalMethodsMutex);
+
+ LLSD methods = LLSD::emptyArray();
+ for (const auto& [method, handler] : mGlobalMethods)
+ {
+ methods.append(method);
+ }
+
+ return methods;
+}
+
+void LLJSONRPCServer::broadcastNotification(const std::string& method, const LLSD& params)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET;
+ // Use custom broadcast logic since we need to call notify() on each JSON-RPC connection
+ // We can't use the base broadcastMessage() because we need structured JSON-RPC messages
+
+ // Create the notification message
+ LLSD notification = LLJSONRPCConnection::makeEnvelope(LLSD(), method, params, LLSD(), LLSD());
+
+ // Use the base class broadcast functionality
+ broadcastMessage(boost::json::serialize(LlsdToJson(notification)));
+
+ // Cache the count: getConnectionCount() walks a locked container in the base.
+ size_t count = getConnectionCount();
+ mTotalNotificationsSent += count;
+ LL_DEBUGS("JSONRPC") << "Broadcast notification: " << method
+ << " to " << count << " clients" << LL_ENDL;
+}
+
+LLSD LLJSONRPCServer::getServerStats() const
+{
+ LLSD stats;
+ stats["server_name"] = mServerName;
+ stats["connection_count"] = static_cast<S32>(getConnectionCount());
+ stats["is_running"] = isRunning();
+
+ {
+ LLMutexLock lock(&mGlobalMethodsMutex);
+ stats["global_method_count"] = static_cast<S32>(mGlobalMethods.size());
+ }
+
+ stats["total_requests_handled"] = static_cast<LLSD::Integer>(mTotalRequestsHandled.load());
+ stats["total_notifications_sent"] = static_cast<LLSD::Integer>(mTotalNotificationsSent.load());
+ stats["uptime"] = LLDate::now().asString();
+
+ return stats;
+}
diff --git a/indra/llcorehttp/lljsonrpcws.h b/indra/llcorehttp/lljsonrpcws.h
new file mode 100644
index 0000000000..98c858b022
--- /dev/null
+++ b/indra/llcorehttp/lljsonrpcws.h
@@ -0,0 +1,547 @@
+/**
+ * @file lljsonrpcws.h
+ * @brief JSON-RPC 2.0 WebSocket server and connection implementation
+ *
+ * $LicenseInfo:firstyear=2025&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2025, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ */
+
+#pragma once
+
+#include "llwebsocketmgr.h"
+#include "llsd.h"
+#include "lluuid.h"
+#include "lltimer.h"
+
+#include <functional>
+#include <unordered_map>
+#include <memory>
+#include <queue>
+
+/**
+ * @class LLJSONRPCConnection
+ * @brief JSON-RPC 2.0 WebSocket connection implementation
+ *
+ * This class implements the JSON-RPC 2.0 protocol over WebSocket connections.
+ * It handles request/response patterns, notifications, method registration,
+ * and error handling according to the JSON-RPC 2.0 specification.
+ *
+ * ## JSON-RPC 2.0 Protocol Features
+ *
+ * - **Requests**: Method calls that expect a response
+ * - **Notifications**: Method calls that do not expect a response
+ * - **Batch Operations**: Multiple requests/notifications in a single message
+ * - **Error Handling**: Standardized error codes and messages
+ * - **ID Correlation**: Request/response correlation using unique identifiers
+ *
+ * ## Method Handler Registration
+ *
+ * Methods use an enhanced handler signature that provides method name and request ID context:
+ *
+ * @code
+ * connection->registerMethod("echo", [](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD {
+ * LL_INFOS("JSONRPC") << "Method " << method << " called with ID " << id.asString() << LL_ENDL;
+ * return params; // Echo back the parameters
+ * });
+ *
+ * connection->registerMethod("add", [](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD {
+ * if (params.isArray() && params.size() >= 2) {
+ * LL_INFOS("JSONRPC") << "Adding numbers via " << method << LL_ENDL;
+ * return params[0].asReal() + params[1].asReal();
+ * }
+ * throw LLJSONRPCConnection::InvalidParams("Expected array with 2 numbers");
+ * });
+ * @endcode
+ *
+ * The enhanced signature enables:
+ * - Method context awareness for shared handlers
+ * - Request correlation and distributed tracing
+ * - Distinction between notifications (id undefined) and requests
+ * - Enhanced logging and error reporting with context
+ *
+ * ## Making RPC Calls
+ *
+ * @code
+ * // Asynchronous request with callback
+ * LLSD params;
+ * params.append(5);
+ * params.append(3);
+ * connection->call("add", params, [](const LLSD& result, const LLSD& error) {
+ * if (error.isUndefined()) {
+ * LL_INFOS() << "Result: " << result.asReal() << LL_ENDL;
+ * } else {
+ * LL_WARNS() << "Error: " << error["message"].asString() << LL_ENDL;
+ * }
+ * });
+ *
+ * // Fire-and-forget notification
+ * connection->notify("log", LLSD("Server started"));
+ * @endcode
+ */
+class LLJSONRPCConnection : public LLWebsocketMgr::WSConnection
+{
+public:
+ using ptr_t = std::shared_ptr<LLJSONRPCConnection>;
+
+ /// Method handler function signature
+ /// @param method The method name that was called
+ /// @param id The request ID (undefined for notifications)
+ /// @param params The parameters passed to the method
+ /// @return The result to return to the caller
+ /// @throw RPCError-derived exceptions for error responses
+ using MethodHandler = std::function<LLSD(const std::string& method, const LLSD& id, const LLSD& params)>;
+
+ /// Response callback function signature
+ /// @param result The result from a successful call (undefined if error occurred)
+ /// @param error The error object if call failed (undefined if successful)
+ using ResponseCallback = std::function<void(const LLSD& result, const LLSD& error)>;
+
+ /**
+ * @brief JSON-RPC error base class
+ */
+ class RPCError : public std::runtime_error
+ {
+ public:
+ // JSON-RPC 2.0 Standard Error Codes
+ static constexpr S32 PARSE_ERROR = -32700; ///< Invalid JSON was received by the server
+ static constexpr S32 INVALID_REQUEST = -32600; ///< The JSON sent is not a valid Request object
+ static constexpr S32 METHOD_NOT_FOUND = -32601; ///< The method does not exist / is not available
+ static constexpr S32 INVALID_PARAMS = -32602; ///< Invalid method parameter(s)
+ static constexpr S32 INTERNAL_ERROR = -32603; ///< Internal JSON-RPC error
+
+ // Server Error Range (-32000 to -32099)
+ static constexpr S32 SERVER_ERROR_MIN = -32099; ///< Server error range minimum
+ static constexpr S32 SERVER_ERROR_MAX = -32000; ///< Server error range maximum
+
+ // Common server-specific errors
+ static constexpr S32 CONNECTION_CLOSED = -32000; ///< Connection closed unexpectedly
+ static constexpr S32 REQUEST_TIMEOUT = -32001; ///< Request timed out
+ static constexpr S32 UNAUTHORIZED = -32002; ///< Authentication required
+ static constexpr S32 FORBIDDEN = -32003; ///< Access denied
+ static constexpr S32 RATE_LIMITED = -32004; ///< Too many requests
+ static constexpr S32 SERVICE_UNAVAILABLE = -32005; ///< Service temporarily unavailable
+ static constexpr S32 MESSAGE_TOO_LARGE = -32006; ///< Message exceeds maximum size
+ static constexpr S32 INVALID_SESSION = -32007; ///< Session expired or invalid
+
+ RPCError(S32 code, const std::string& message, const LLSD& data = LLSD())
+ : std::runtime_error(message), mCode(code), mData(data) {}
+
+ S32 getCode() const { return mCode; }
+ const LLSD& getData() const { return mData; }
+
+ protected:
+ S32 mCode;
+ LLSD mData;
+ };
+
+ /// Standard JSON-RPC error classes using named constants
+ class ParseError : public RPCError {
+ public:
+ ParseError(const std::string& details = "")
+ : RPCError(PARSE_ERROR, "Parse error" + (details.empty() ? "" : ": " + details)) {}
+ };
+
+ class InvalidRequest : public RPCError {
+ public:
+ InvalidRequest(const std::string& details = "")
+ : RPCError(INVALID_REQUEST, "Invalid Request" + (details.empty() ? "" : ": " + details)) {}
+ };
+
+ class MethodNotFound : public RPCError {
+ public:
+ MethodNotFound(const std::string& method = "")
+ : RPCError(METHOD_NOT_FOUND, "Method not found" + (method.empty() ? "" : ": " + method)) {}
+ };
+
+ class InvalidParams : public RPCError {
+ public:
+ InvalidParams(const std::string& details = "")
+ : RPCError(INVALID_PARAMS, "Invalid params" + (details.empty() ? "" : ": " + details)) {}
+ };
+
+ class InternalError : public RPCError {
+ public:
+ InternalError(const std::string& details = "")
+ : RPCError(INTERNAL_ERROR, "Internal error" + (details.empty() ? "" : ": " + details)) {}
+ };
+
+ /// Server-specific errors (in the -32000 to -32099 range)
+ class ConnectionClosedError : public RPCError {
+ public:
+ ConnectionClosedError(const std::string& details = "Connection closed")
+ : RPCError(CONNECTION_CLOSED, details) {}
+ };
+
+ class RequestTimeoutError : public RPCError {
+ public:
+ RequestTimeoutError(const std::string& details = "Request timed out")
+ : RPCError(REQUEST_TIMEOUT, details) {}
+ };
+
+ class UnauthorizedError : public RPCError {
+ public:
+ UnauthorizedError(const std::string& details = "Authentication required")
+ : RPCError(UNAUTHORIZED, details) {}
+ };
+
+ class ForbiddenError : public RPCError {
+ public:
+ ForbiddenError(const std::string& details = "Access denied")
+ : RPCError(FORBIDDEN, details) {}
+ };
+
+ class RateLimitedError : public RPCError {
+ public:
+ RateLimitedError(const std::string& details = "Too many requests")
+ : RPCError(RATE_LIMITED, details) {}
+ };
+
+ class ServiceUnavailableError : public RPCError {
+ public:
+ ServiceUnavailableError(const std::string& details = "Service temporarily unavailable")
+ : RPCError(SERVICE_UNAVAILABLE, details) {}
+ };
+
+ class InvalidSessionError : public RPCError {
+ public:
+ InvalidSessionError(const std::string& details = "Session expired or invalid")
+ : RPCError(INVALID_SESSION, details) {}
+ };
+
+
+ LLJSONRPCConnection(const LLWebsocketMgr::WSServer::ptr_t server,
+ const LLWebsocketMgr::connection_h& handle)
+ : LLWebsocketMgr::WSConnection(server, handle) {}
+
+ ~LLJSONRPCConnection() override = default;
+
+ // WebSocket connection lifecycle
+ void onOpen() override;
+ void onClose() override;
+ void onMessage(const std::string& message) override;
+
+ /**
+ * @brief Register a method handler
+ * @param method The method name to register
+ * @param handler The function to call when this method is invoked
+ *
+ * @warning Sync handlers execute on the WebSocket I/O thread. They must
+ * only touch state that is either internal to this connection
+ * (protected by the connection's mutex) or otherwise thread-safe.
+ * Do NOT read or write viewer main-thread-only state (e.g.,
+ * gAgent, gObjectList, LLSelectMgr, LLFloaterReg, gSavedSettings,
+ * LLInventoryModel, or any LLViewerObject) from a sync handler;
+ * register with registerAsyncMethod() instead, which dispatches
+ * to the main thread inside a coroutine.
+ */
+ void registerMethod(const std::string& method, MethodHandler handler);
+
+ /**
+ * @brief Register an async method handler, executed in a coroutine
+ *
+ * Unlike registerMethod(), the handler runs inside an LLCoros coroutine
+ * and may use llcoro::suspendUntilEventOn* to wait for async results.
+ * The handler returns its result normally; the framework sends the
+ * JSON-RPC response automatically when the coroutine returns.
+ *
+ * @param method The method name to register
+ * @param handler The coroutine-safe function to call
+ */
+ void registerAsyncMethod(const std::string& method, MethodHandler handler);
+
+ /**
+ * @brief Unregister a method handler
+ * @param method The method name to unregister
+ */
+ void unregisterMethod(const std::string& method);
+
+ /**
+ * @brief Get the names of all registered methods
+ * @return Ordered set containing sync and async method names
+ */
+ virtual std::set<std::string> getMethods() const;
+
+ /**
+ * @brief Make an asynchronous JSON-RPC call
+ * @param method The method name to call
+ * @param params The parameters to pass
+ * @param callback Callback for the response (optional)
+ * @param timeout Seconds before the callback is told the call timed out
+ * @return The request ID for correlation, undefined where it could not
+ * be sent (the callback is then never called)
+ */
+ LLSD call(const std::string& method, const LLSD& params = LLSD(),
+ ResponseCallback callback = nullptr, F64 timeout = REQUEST_TIMEOUT_SECONDS);
+
+ /**
+ * @brief Whether the peer's requests are dispatched
+ *
+ * A connection starts out unauthenticated and refuses every request as
+ * unauthorized, whether or not the method exists, until it is told the
+ * peer has proven itself -- e.g. by answering a challenge. Answers to
+ * its own calls go through either way: that is how the peer proves
+ * itself.
+ */
+ bool isAuthenticated() const { return mAuthenticated.load(); }
+
+ /**
+ * @brief Send a JSON-RPC notification (no response expected)
+ * @param method The method name
+ * @param params The parameters to pass
+ */
+ bool notify(const std::string& method, const LLSD& params = LLSD());
+
+ /**
+ * @brief Send a successful response to a request
+ * @param id The request ID from the original request
+ * @param result The result to return
+ */
+ bool sendResponse(const LLSD& id, const LLSD& result);
+
+ /**
+ * @brief Send an error response to a request
+ * @param id The request ID from the original request (can be null)
+ * @param error The RPCError to send
+ */
+ bool sendError(const LLSD& id, const RPCError& error);
+
+ /**
+ * @brief Send a batch of requests/notifications
+ * @param batch Array of request/notification objects
+ * @param callback Callback for batch response (optional)
+ */
+ bool sendBatch(const LLSD& batch, ResponseCallback callback = nullptr);
+
+protected:
+ /**
+ * @brief Process a single JSON-RPC message
+ * @param message_obj The parsed JSON message
+ */
+ void processMessage(const LLSD& message_obj);
+
+ /**
+ * @brief Process a JSON-RPC request
+ * @param request The request object
+ */
+ void processRequest(const LLSD& request);
+
+ /**
+ * @brief Process a JSON-RPC response
+ * @param response The response object
+ */
+ void processResponse(const LLSD& response);
+
+ /**
+ * @brief Validate a JSON-RPC message structure
+ * @param message The message to validate
+ * @param is_request True if validating a request, false for response
+ */
+ bool validateMessage(const LLSD& message, bool is_request = true);
+
+ /**
+ * @brief Generate the next unique request ID
+ * @return A server-unique request ID
+ *
+ * Generates a server-wide unique identifier using an atomic counter.
+ * This ensures request IDs are unique across all connections within the
+ * server instance, providing efficient ID generation with guaranteed uniqueness.
+ *
+ * IDs follow the format "rpc_{counter}" where counter is a monotonically
+ * increasing 64-bit value starting from 1. This approach provides:
+ * - Guaranteed uniqueness within server scope
+ * - High performance (atomic increment operation)
+ * - Predictable, sequential ordering for debugging
+ * - Thread-safe generation across multiple connections
+ */
+ LLSD generateId();
+
+ void setAuthenticated(bool authenticated) { mAuthenticated = authenticated; }
+
+public:
+ /**
+ * @brief Build a JSON-RPC 2.0 envelope.
+ *
+ * Stamps "jsonrpc" = "2.0" and includes only the fields that are set:
+ * - @a method is included when non-empty.
+ * - @a params, @a result, @a error are included when defined.
+ * - @a id is included unless it is undefined and @a method is non-empty
+ * (i.e. notifications omit id; responses keep id, serializing an
+ * undefined id as JSON null per the JSON-RPC spec).
+ */
+ static LLSD makeEnvelope(const LLSD& id,
+ const std::string& method,
+ const LLSD& params,
+ const LLSD& result,
+ const LLSD& error);
+
+private:
+ // Guards the three maps below. Handlers/callbacks are copied out from
+ // under the lock and then invoked without it held, to avoid re-entrancy
+ // and to keep the critical section short.
+ mutable LLMutex mMutex;
+ std::unordered_map<std::string, MethodHandler> mMethodHandlers;
+ std::unordered_map<std::string, MethodHandler> mAsyncMethodHandlers;
+ std::unordered_map<std::string, ResponseCallback> mPendingRequests;
+
+ // Per-request timeout tracking. mPendingDeadlines is a min-heap of
+ // (deadline, request_id) ordered by deadline; entries whose request has
+ // already been answered become tombstones (skipped when they reach the
+ // top). The owning server calls sweepTimeouts() once per frame (see
+ // LLJSONRPCServer::update()) to fire any expired entries.
+ struct PendingDeadline
+ {
+ F64 mDeadline; // absolute time in seconds (LLTimer::getTotalSeconds)
+ std::string mId;
+ // std::priority_queue is a max-heap; invert to get min-heap by deadline.
+ bool operator<(const PendingDeadline& rhs) const { return mDeadline > rhs.mDeadline; }
+ };
+ std::priority_queue<PendingDeadline> mPendingDeadlines;
+
+ std::atomic<bool> mAuthenticated{ false };
+
+ static constexpr F64 REQUEST_TIMEOUT_SECONDS = 120.0;
+
+public:
+ /// Sweep interval used by LLJSONRPCServer::update() to throttle sweepTimeouts().
+ static constexpr F32 TIMEOUT_SWEEP_INTERVAL = 1.0f;
+
+ /// Invoked once per frame by the owning server (see LLJSONRPCServer::update());
+ /// fires the timeout callback for any request whose deadline has passed.
+ /// Main-thread only.
+ void sweepTimeouts();
+
+ void testInjectPendingRequest(const std::string& id, F64 deadline, ResponseCallback callback);
+ void testSweepTimeouts();
+ size_t testPendingRequestCount() const;
+};
+
+/**
+ * @class LLJSONRPCServer
+ * @brief JSON-RPC 2.0 WebSocket server implementation
+ *
+ * This server extends the basic WebSocket server to provide JSON-RPC 2.0
+ * protocol support. It manages JSON-RPC connections and provides server-wide
+ * method registration and broadcasting capabilities.
+ *
+ * ## Server-Wide Method Registration
+ *
+ * Methods can be registered at the server level and will be available
+ * on all connections:
+ *
+ * @code
+ * auto server = std::make_shared<LLJSONRPCServer>("rpc_server", 8080);
+ *
+ * server->registerGlobalMethod("getServerInfo", [](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD {
+ * LL_INFOS("JSONRPC") << "Server info requested via " << method << LL_ENDL;
+ * LLSD info;
+ * info["name"] = "My RPC Server";
+ * info["version"] = "1.0.0";
+ * info["uptime"] = LLDate::now().secondsSinceEpoch();
+ * return info;
+ * });
+ *
+ * server->registerGlobalMethod("listMethods", [server](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD {
+ * return server->getMethodList();
+ * });
+ * @endcode
+ *
+ * ## Broadcasting and Multi-client Operations
+ *
+ * @code
+ * // Broadcast notification to all connected clients
+ * server->broadcastNotification("serverAlert", LLSD("Server will restart in 5 minutes"));
+ * @endcode
+ */
+class LLJSONRPCServer : public LLWebsocketMgr::WSServer
+{
+public:
+ using ptr_t = std::shared_ptr<LLJSONRPCServer>;
+ using MethodHandler = LLJSONRPCConnection::MethodHandler;
+ using ResponseCallback = LLJSONRPCConnection::ResponseCallback;
+
+ LLJSONRPCServer(const std::string& name, U16 port, bool local_only = true);
+ ~LLJSONRPCServer() override = default;
+
+ // Server lifecycle callbacks
+ void onConnectionOpened(const LLWebsocketMgr::WSConnection::ptr_t& connection) override;
+ void onConnectionClosed(const LLWebsocketMgr::WSConnection::ptr_t& connection) override;
+
+ /**
+ * @brief Register a global method available on all connections
+ * @param method The method name to register
+ * @param handler The function to call when this method is invoked
+ */
+ void registerGlobalMethod(const std::string& method, MethodHandler handler);
+
+ /**
+ * @brief Unregister a global method
+ * @param method The method name to unregister
+ */
+ void unregisterGlobalMethod(const std::string& method);
+
+ /**
+ * @brief Get list of registered global methods
+ * @return Array of method names
+ */
+ LLSD getMethodList() const;
+
+ /**
+ * @brief Broadcast a notification to all connected clients
+ * @param method The method name
+ * @param params The parameters to pass
+ */
+ void broadcastNotification(const std::string& method, const LLSD& params = LLSD());
+
+ /**
+ * @brief Get server statistics
+ * @return Statistics object with connection count, method count, etc.
+ */
+ LLSD getServerStats() const;
+
+ bool update() override;
+
+protected:
+ LLWebsocketMgr::WSConnection::ptr_t connectionFactory(LLWebsocketMgr::WSServer::ptr_t server,
+ LLWebsocketMgr::connection_h handle) override;
+
+ virtual LLSD handlePing(const LLJSONRPCConnection::ptr_t& connection,
+ const LLSD& params) const;
+
+ virtual LLSD handleGetVersion(const LLJSONRPCConnection::ptr_t& connection,
+ const LLSD& params) const = 0;
+ virtual LLSD handleStatus(const LLJSONRPCConnection::ptr_t& connection,
+ const LLSD& params) const;
+
+ /**
+ * @brief Apply global method handlers to a new connection
+ * @param connection The connection to configure
+ */
+ virtual void setupConnectionMethods(LLJSONRPCConnection::ptr_t connection);
+
+private:
+ std::unordered_map<std::string, MethodHandler> mGlobalMethods;
+ mutable LLMutex mGlobalMethodsMutex;
+
+ std::string mServerName; // Store server name for stats
+ std::atomic<U64> mTotalRequestsHandled{0};
+ std::atomic<U64> mTotalNotificationsSent{0};
+ LLTimer mSweepTimer;
+};
diff --git a/indra/llcorehttp/llwebsocketmgr.cpp b/indra/llcorehttp/llwebsocketmgr.cpp
new file mode 100644
index 0000000000..ef07295eed
--- /dev/null
+++ b/indra/llcorehttp/llwebsocketmgr.cpp
@@ -0,0 +1,776 @@
+/**
+ * @file llwebsocketmgr.cpp
+ * @brief WebSocket manager singleton implementation
+ *
+ * $LicenseInfo:firstyear=2025&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2025, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ */
+
+#include "linden_common.h"
+
+#include "llwebsocketmgr.h"
+#include "llerror.h"
+#include "llsdserialize.h"
+#include "llhost.h"
+#include "llsdjson.h"
+#include "lltimer.h"
+
+#include <websocketpp/config/boost_config.hpp>
+#include <websocketpp/config/asio_no_tls.hpp>
+#include <websocketpp/client.hpp>
+#include <websocketpp/server.hpp>
+
+#include <thread>
+#include <atomic>
+#include <chrono>
+
+//------------------------------------------------------------------------
+namespace
+{
+ using Server_t = websocketpp::server<websocketpp::config::asio>;
+ using Client_t = websocketpp::client<websocketpp::config::asio>;
+ using Connection_t = websocketpp::connection<websocketpp::config::asio>;
+
+ // How long stop() waits for the I/O thread to exit before logging a
+ // warning. It still joins after this -- this is diagnostic, not a timeout.
+ constexpr F32 SERVER_STOP_WARN_SECONDS = 5.0f;
+}
+
+//------------------------------------------------------------------------
+
+//------------------------------------------------------------------------
+// LLWebsocketMgr Implementation
+//
+
+void LLWebsocketMgr::initSingleton()
+{ }
+
+void LLWebsocketMgr::cleanupSingleton()
+{
+ stopAllServers();
+}
+
+void LLWebsocketMgr::update()
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET;
+ std::vector<WSServer::ptr_t> stops;
+
+ for (auto &[name, server] : mServers)
+ {
+ if (server && server->isRunning())
+ {
+ if (!server->update())
+ {
+ stops.push_back(server);
+ }
+ }
+ }
+
+ for (const auto& server : stops)
+ {
+ if (server)
+ {
+ LL_DEBUGS("WebSocket") << "Stopping server: " << server->mServerName << LL_ENDL;
+ removeServer(server->mServerName);
+ }
+ }
+}
+
+
+LLWebsocketMgr::WSServer::ptr_t LLWebsocketMgr::findServerByName(const std::string &name) const
+{
+ auto it = mServers.find(std::string(name));
+ if (it != mServers.end())
+ {
+ return it->second;
+ }
+ return nullptr;
+}
+
+bool LLWebsocketMgr::addServer(const LLWebsocketMgr::WSServer::ptr_t& server)
+{
+ if (!server)
+ {
+ LL_WARNS("WebSocket") << "Attempted to add a null server" << LL_ENDL;
+ return false;
+ }
+
+ auto it = mServers.find(server->mServerName);
+ if (it != mServers.end())
+ {
+ LL_WARNS("WebSocket") << "Server with name " << server->mServerName << " already exists" << LL_ENDL;
+ return false;
+ }
+ mServers[server->mServerName] = server;
+ LL_INFOS("WebSocket") << "Added WebSocket server: " << server->mServerName << LL_ENDL;
+ return true;
+}
+
+bool LLWebsocketMgr::removeServer(const std::string& name)
+{
+ auto it = mServers.find(name);
+ if (it == mServers.end())
+ {
+ LL_WARNS("WebSocket") << "No server found with name " << name << " to remove" << LL_ENDL;
+ return false;
+ }
+ if (it->second && it->second->isRunning())
+ it->second->stop();
+ mServers.erase(it);
+ LL_INFOS("WebSocket") << "Removed WebSocket server: " << name << LL_ENDL;
+ return true;
+}
+
+
+bool LLWebsocketMgr::startServer(const std::string &name) const
+{
+ LLWebsocketMgr::WSServer::ptr_t server = findServerByName(name);
+ if (!server)
+ {
+ LL_WARNS("WebSocket") << "No server found with name " << name << " to start" << LL_ENDL;
+ return false;
+ }
+ if (server->isRunning())
+ {
+ LL_WARNS("WebSocket") << "Server " << name << " is already running" << LL_ENDL;
+ return false;
+ }
+ return server->start();
+}
+
+void LLWebsocketMgr::stopServer(const std::string& name) const
+{
+ LLWebsocketMgr::WSServer::ptr_t server = findServerByName(name);
+ if (!server)
+ {
+ LL_WARNS("WebSocket") << "No server found with name " << name << " to stop" << LL_ENDL;
+ return;
+ }
+ if (!server->isRunning())
+ {
+ LL_WARNS("WebSocket") << "Server " << name << " is not running" << LL_ENDL;
+ return;
+ }
+ server->stop();
+}
+
+void LLWebsocketMgr::stopAllServers()
+{
+ for (auto &[name, server] : mServers)
+ {
+ if (server && server->isRunning())
+ {
+ LL_INFOS("WebSocket") << "Stopping server: " << name << LL_ENDL;
+ server->stop();
+ }
+ }
+
+ mServers.clear();
+}
+
+//------------------------------------------------------------------------
+struct Server_impl
+{
+ Server_impl(LLWebsocketMgr::WSServer *owner, U16 port, bool local_only) :
+ mOwner(owner),
+ mPort(port),
+ mLocalOnly(local_only)
+ {
+
+ mServer.set_validate_handler([this](websocketpp::connection_hdl hdl) { return this->onValidate(hdl); });
+ mServer.set_open_handler([this](websocketpp::connection_hdl hdl) { this->onOpen(hdl); });
+ mServer.set_close_handler([this](websocketpp::connection_hdl hdl) { this->onClose(hdl); });
+ mServer.set_message_handler([this](websocketpp::connection_hdl hdl, Server_t::message_ptr msg) { this->onMessage(hdl, msg); });
+ }
+
+ ~Server_impl() = default;
+
+ /**
+ * @brief Initialize the websocketpp server and configure listening
+ *
+ * Performs the initial setup of the websocketpp server by calling init_asio()
+ * to initialize the ASIO networking layer, then configures the server to listen
+ * on the specified port. The binding behavior depends on the mLocalOnly flag:
+ * - If mLocalOnly is true: binds to "127.0.0.1" (localhost only)
+ * - If mLocalOnly is false: binds to all available network interfaces
+ */
+ void init()
+ {
+ mServer.init_asio();
+ try {
+ if (mLocalOnly)
+ {
+ std::stringstream port_str;
+ port_str << mPort;
+ mServer.listen("127.0.0.1", port_str.str());
+ }
+ else
+ {
+ mServer.listen(mPort);
+ }
+ }
+ catch (const websocketpp::exception& e)
+ {
+ LL_WARNS("WebSocket") << "WebSocket server listen exception: " << e.what() << LL_ENDL;
+ }
+ catch (const std::exception& e)
+ {
+ LL_WARNS("WebSocket") << "WebSocket server listen std::exception: " << e.what() << LL_ENDL;
+ }
+ catch (...)
+ {
+ LL_WARNS("WebSocket") << "WebSocket server listen unknown exception" << LL_ENDL;
+ }
+ }
+
+ /**
+ * @brief Start the websocket server and begin accepting connections
+ * @return true if server started successfully, false on error
+ */
+ bool start()
+ {
+ //if (!mServer.stopped())
+ //{
+ // LL_WARNS("WebSocket") << "WebSocket server is already running" << LL_ENDL;
+ // return false;
+ //}
+
+ try
+ {
+ LL_INFOS("WebSocket") << "Starting WebSocket server on port " << mPort
+ << (mLocalOnly ? " (localhost only)" : " (all interfaces)") << LL_ENDL;
+ mServer.start_accept();
+
+ // Run controlled event loop with periodic stop flag checking
+ while (!mOwner->mShouldStop && !mServer.stopped())
+ {
+ LL_PROFILE_ZONE_NAMED_CATEGORY_WEBSOCKET("ws server run_for");
+ // Process events for up to 100ms, then check the stop flag
+ std::chrono::milliseconds timeout(100);
+ std::size_t handlers_run = mServer.get_io_service().run_for(timeout);
+
+ // If no handlers were run and the server isn't stopped,
+ // reset the io_service for the next iteration
+ if (handlers_run == 0 && !mServer.stopped() && !mOwner->mShouldStop)
+ {
+ mServer.get_io_service().restart();
+ }
+ }
+
+ LL_INFOS("WebSocket") << "WebSocket server event loop exited cleanly" << LL_ENDL;
+ return true;
+ }
+ catch (const websocketpp::exception& e)
+ {
+ LL_WARNS("WebSocket") << "WebSocket server exception: " << e.what() << LL_ENDL;
+ return false;
+ }
+ catch (const std::exception& e)
+ {
+ LL_WARNS("WebSocket") << "WebSocket server std::exception: " << e.what() << LL_ENDL;
+ return false;
+ }
+ catch (...)
+ {
+ LL_WARNS("WebSocket") << "WebSocket server unknown exception" << LL_ENDL;
+ return false;
+ }
+ }
+
+ void stop()
+ {
+ if (mServer.stopped())
+ {
+ return;
+ }
+ try
+ {
+ mServer.stop_listening();
+ mServer.stop();
+ }
+ catch (const std::exception&)
+ {
+ LL_WARNS("WebSocket") << "Error stopping WebSocket server" << LL_ENDL;
+ }
+ }
+
+ /**
+ * @brief Decide whether an upgrade request is let in
+ * @param hdl WebSocket connection handle from websocketpp
+ * @return false to answer the request 403 and never open it
+ *
+ * Called by websocketpp once the request has been read, before it is
+ * answered: the owner says which origins it takes.
+ */
+ bool onValidate(websocketpp::connection_hdl hdl)
+ {
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET;
+ websocketpp::lib::error_code ec;
+ Server_t::connection_ptr con = mServer.get_con_from_hdl(hdl, ec);
+ if (ec || !con)
+ {
+ return false;
+ }
+ const std::string& origin = con->get_origin();
+ if (mOwner->acceptOrigin(origin))
+ {
+ return true;
+ }
+ // Once for each origin: a page can ask as often as it likes.
+ LL_WARNS_ONCE("WebSocket") << mOwner->mServerName << " refused a connection from origin " << origin << LL_ENDL;
+ con->set_status(websocketpp::http::status_code::forbidden);
+ return false;
+ }
+
+ /**
+ * @brief Handle new connection establishment event
+ * @param hdl WebSocket connection handle from websocketpp
+ *
+ * Called automatically by the websocketpp library when a new client connection
+ * is successfully established. This method serves as a bridge between the
+ * low-level websocketpp callback and the high-level WSServer interface.
+ */
+ void onOpen(websocketpp::connection_hdl hdl) const
+ {
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET;
+ LL_ERRS_IF(!mOwner, "WebSocket") << "mOwner should never be null. If it is, something is very wrong!" << LL_ENDL;
+
+ mOwner->handleOpenConnection(hdl);
+ }
+
+ /**
+ * @brief Handle connection closure event
+ * @param hdl WebSocket connection handle from websocketpp
+ */
+ void onClose(websocketpp::connection_hdl hdl) const
+ {
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET;
+ LL_ERRS_IF(!mOwner, "WebSocket") << "mOwner should never be null" << LL_ENDL;
+ mOwner->handleCloseConnection(hdl);
+ }
+
+ /**
+ * @brief Handle incoming message from client
+ * @param hdl WebSocket connection handle identifying the sender
+ * @param msg Shared pointer to the message object containing payload and metadata
+ *
+ * Called automatically by the websocketpp library when a complete message is
+ * received from a client. This method validates the connection exists, extracts
+ * the message payload, and forwards it to the appropriate connection handler.
+ *
+ * Currently handles text messages only.
+ */
+ void onMessage(websocketpp::connection_hdl hdl, Server_t::message_ptr msg) const
+ {
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET;
+ LL_ERRS_IF(!mOwner, "WebSocket") << "mOwner should never be null" << LL_ENDL;
+ LLWebsocketMgr::WSConnection::ptr_t connection = mOwner->getConnection(hdl);
+ if (!connection)
+ {
+ LL_WARNS("WebSocket") << "Received message for unknown connection" << LL_ENDL;
+ return;
+ }
+
+ // TODO: check the FIN bit and handle fragmented messages if needed
+ // TODO: check terminal and close codes and handle connection closure if needed
+ mOwner->handleMessage(hdl, msg->get_payload());
+ }
+
+ //-------------------------------------------
+ Server_t mServer; ///< The underlying websocketpp server instance
+ LLWebsocketMgr::WSServer* mOwner{ nullptr }; ///< Back-reference to the owning WSServer instance (guaranteed non-null)
+ U16 mPort{ 0 }; ///< TCP port number the server listens on
+ bool mLocalOnly{ true }; ///< Whether to bind to localhost only (true) or all interfaces (false)
+};
+
+//------------------------------------------------------------------------
+LLWebsocketMgr::WSServer::WSServer(std::string_view name, U16 port, bool local_only):
+ mServerName(name),
+ mImpl(std::make_unique<Server_impl>(this, port, local_only))
+{
+ mImpl->init();
+
+ // Initialize the server with the given name and host
+ LL_INFOS("WebSocket") << "Creating WebSocket server: " << name <<
+ " listening " << (mImpl->mLocalOnly ? "locally" : "ON ALL INTERFACES") <<
+ " on port " << mImpl->mPort << LL_ENDL;
+}
+
+LLWebsocketMgr::WSServer::~WSServer()
+{
+ // Ensure the server is stopped before destruction
+ stop();
+}
+
+LLWebsocketMgr::WSConnection::ptr_t LLWebsocketMgr::WSServer::connectionFactory(LLWebsocketMgr::WSServer::ptr_t server,
+ LLWebsocketMgr::connection_h handle)
+{
+ return std::make_shared<LLWebsocketMgr::WSConnection>(server, handle);
+}
+
+bool LLWebsocketMgr::WSServer::start()
+{
+ LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL;
+
+ LLMutexLock lock(&mThreadMutex);
+
+ // Check if already running
+ if (isRunning())
+ {
+ LL_WARNS("WebSocket") << "Server " << mServerName << " is already running" << LL_ENDL;
+ return false;
+ }
+
+ // Reset the stop flag
+ mShouldStop = false;
+ mRunning = true;
+
+ // Start the server thread
+ mServerThread = std::thread([this]() {
+ LL_PROFILER_SET_THREAD_NAME(mServerName.c_str());
+ LL_INFOS("WebSocket") << "WebSocket server thread starting for: " << mServerName << LL_ENDL;
+
+ // Run the controlled server loop that checks the stop flag
+ // Server_impl accesses mShouldStop through the mOwner pointer
+ bool success = mImpl->start();
+
+ if (!success)
+ {
+ LL_WARNS("WebSocket") << "WebSocket server thread failed to start for: " << mServerName << LL_ENDL;
+ }
+
+ LL_INFOS("WebSocket") << "WebSocket server thread exiting for: " << mServerName << LL_ENDL;
+ mRunning = false;
+ });
+
+ onStarted();
+ LL_INFOS("WebSocket") << "Started WebSocket server thread: " << mServerName << LL_ENDL;
+ return true;
+}
+
+void LLWebsocketMgr::WSServer::stop()
+{
+ LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL;
+
+ {
+ LLMutexLock lock(&mThreadMutex);
+
+ // Check if already stopped
+ if (!isRunning())
+ {
+ return;
+ }
+
+ LL_INFOS("WebSocket") << "Stopping WebSocket server: " << mServerName << LL_ENDL;
+
+ mShouldStop = true;
+
+ // Send close frames to all connected clients before stopping the ASIO loop
+ closeAllConnections(1001, "Server shutting down");
+
+ // Stop the websocket server (this will cause the controlled run loop to exit)
+ mImpl->stop();
+ } // Release the lock here
+
+ if (mServerThread.joinable())
+ {
+ // The I/O thread should exit promptly once mImpl->stop() returns. If
+ // it doesn't, something on that thread is blocked -- most likely a
+ // regression reintroducing a cross-thread wait. Warn instead of
+ // hanging silently; still join (detaching would use-after-free mImpl).
+ LLTimer wait_timer;
+ while (mRunning && wait_timer.getElapsedTimeF32() < SERVER_STOP_WARN_SECONDS)
+ {
+ ms_sleep(10);
+ }
+ if (mRunning)
+ {
+ LL_WARNS("WebSocket") << mServerName << ": I/O thread did not exit within "
+ << SERVER_STOP_WARN_SECONDS << "s; joining anyway. This indicates a "
+ << "thread-boundary regression." << LL_ENDL;
+ }
+
+ mServerThread.join();
+ LL_INFOS("WebSocket") << "WebSocket server thread joined for: " << mServerName << LL_ENDL;
+ }
+ onStopped();
+}
+
+bool LLWebsocketMgr::WSServer::isRunning() const
+{
+ // mRunning is set true just before the I/O thread launches and false as
+ // the last thing that thread does before exiting -- lock-free, and safe
+ // to call while start()/stop() hold mThreadMutex on this thread (unlike
+ // a mutex-based check, which would self-deadlock there).
+ return mRunning && !mShouldStop;
+}
+
+void LLWebsocketMgr::WSServer::broadcastMessage(const std::string& message)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET;
+ LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL;
+ LLMutexLock lock(&mConnectionMutex);
+ for (const auto& [handle, conn] : mConnections)
+ {
+ sendMessageTo(handle, message);
+ }
+}
+
+bool LLWebsocketMgr::WSServer::sendMessageTo(const connection_h& handle, const std::string& message)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET;
+ LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL;
+ websocketpp::lib::error_code ec;
+ mImpl->mServer.send(handle, message, websocketpp::frame::opcode::text, ec);
+ if (ec)
+ {
+ LL_WARNS("WebSocket") << mServerName << " failed to send message: " << ec.message() << LL_ENDL;
+ return false;
+ }
+ return true;
+}
+
+bool LLWebsocketMgr::WSServer::closeConnection(const connection_h& handle, U16 code, const std::string& reason)
+{
+ LL_ERRS_IF(!mImpl, "WebSocket") << "WebSocket server " << mServerName << " implementation is null !" << LL_ENDL;
+
+ try
+ {
+ websocketpp::lib::error_code ec;
+ mImpl->mServer.close(handle, code, reason, ec);
+ if (ec)
+ {
+ LL_WARNS("WebSocket") << mServerName << " failed to close connection: " << ec.message() << LL_ENDL;
+ return false;
+ }
+
+ LL_INFOS("WebSocket") << mServerName << " initiated close for connection with code "
+ << code << " and reason: " << reason << LL_ENDL;
+ return true;
+ }
+ catch (const websocketpp::exception& e)
+ {
+ LL_WARNS("WebSocket") << mServerName << " exception closing connection: " << e.what() << LL_ENDL;
+ return false;
+ }
+ catch (const std::exception& e)
+ {
+ LL_WARNS("WebSocket") << mServerName << " std::exception closing connection: " << e.what() << LL_ENDL;
+ return false;
+ }
+ catch (...)
+ {
+ LL_WARNS("WebSocket") << mServerName << " unknown exception closing connection" << LL_ENDL;
+ return false;
+ }
+}
+
+LLWebsocketMgr::WSConnection::ptr_t LLWebsocketMgr::WSServer::getConnection(const connection_h& handle)
+{
+ LLMutexLock lock(&mConnectionMutex);
+ auto it = mConnections.find(handle);
+ if (it != mConnections.end())
+ {
+ return it->second;
+ }
+ return nullptr;
+}
+
+std::vector<LLWebsocketMgr::WSConnection::ptr_t> LLWebsocketMgr::WSServer::getConnections() const
+{
+ LLMutexLock lock(&mConnectionMutex);
+ std::vector<WSConnection::ptr_t> result;
+ result.reserve(mConnections.size());
+ for (const auto& [handle, conn] : mConnections)
+ {
+ result.push_back(conn);
+ }
+ return result;
+}
+
+LLWebsocketMgr::connection_state_t LLWebsocketMgr::WSServer::getConnectionState(const connection_h& handle) const
+{
+ websocketpp::lib::error_code ec;
+ auto con = mImpl->mServer.get_con_from_hdl(handle, ec);
+ if (ec)
+ {
+ LL_WARNS("WebSocket") << mServerName << " failed to get connection state: " << ec.message() << LL_ENDL;
+ websocketpp::session::state::value state = websocketpp::session::state::closed;
+ return connection_closed;
+ }
+ return static_cast<connection_state_t>(con->get_state());
+}
+
+
+void LLWebsocketMgr::WSServer::handleOpenConnection(const connection_h& handle)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET;
+ WSConnection::ptr_t connection;
+ size_t size(0);
+ {
+ LLMutexLock lock(&mConnectionMutex);
+ auto it = mConnections.find(handle);
+ if (it == mConnections.end())
+ {
+ connection = connectionFactory(shared_from_this(), handle);
+ if (!connection)
+ {
+ LL_WARNS("WebSocket") << "Failed to create connection for websocket server " << mServerName << LL_ENDL;
+ return;
+ }
+ mConnections[handle] = connection;
+ }
+ else
+ {
+ connection = it->second;
+ }
+
+ if (!connection)
+ {
+ LL_WARNS("WebSocket") << mServerName << " failed to create connection object" << LL_ENDL;
+ return;
+ }
+ // Removed redundant assignment to mConnections[handle]
+ size = mConnections.size();
+ }
+
+ onConnectionOpened(connection); // TODO: consider letting the server reject the connection here
+ connection->onOpen();
+ LL_INFOS("WebSocket") << mServerName << " opened new connection, total connections: " << size << LL_ENDL;
+}
+
+void LLWebsocketMgr::WSServer::handleCloseConnection(const connection_h& handle)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET;
+ size_t size(0);
+ WSConnection::ptr_t connection;
+ {
+ LLMutexLock lock(&mConnectionMutex);
+ auto it = mConnections.find(handle);
+ if (it != mConnections.end())
+ {
+ connection = it->second;
+ mConnections.erase(it);
+ }
+ size = mConnections.size();
+ }
+ if (connection)
+ {
+ connection->onClose();
+ onConnectionClosed(connection);
+ LL_INFOS("WebSocket") << mServerName << " closed connection, total connections: " << size << LL_ENDL;
+ }
+ else
+ {
+ LL_WARNS("WebSocket") << mServerName << " attempted to close unknown connection" << LL_ENDL;
+ }
+}
+
+void LLWebsocketMgr::WSServer::handleMessage(const connection_h& handle, const std::string& message)
+{
+ LL_PROFILE_ZONE_SCOPED_CATEGORY_WEBSOCKET;
+ WSConnection::ptr_t connection = getConnection(handle);
+ if (connection)
+ {
+ connection->onMessage(message);
+ }
+ else
+ {
+ LL_WARNS("WebSocket") << mServerName << " received message for unknown connection" << LL_ENDL;
+ }
+}
+
+//------------------------------------------------------------------------
+bool LLWebsocketMgr::WSConnection::sendMessage(const std::string& message) const
+{
+ if (mOwningServer.expired())
+ {
+ LL_WARNS("WebSocket") << "Attempted to send message on connection with null server reference" << LL_ENDL;
+ return false;
+ }
+ return mOwningServer.lock()->sendMessageTo(mConnectionHandle, message);
+}
+
+bool LLWebsocketMgr::WSConnection::sendMessage(const boost::json::value& json) const
+{
+ std::string message = boost::json::serialize(json);
+ return sendMessage(message);
+}
+
+bool LLWebsocketMgr::WSConnection::sendMessage(const LLSD& data) const
+{
+ return sendMessage(LlsdToJson(data));
+}
+
+void LLWebsocketMgr::WSConnection::closeConnection(U16 code, const std::string& reason)
+{
+ if (mOwningServer.expired())
+ {
+ LL_WARNS("WebSocket") << "Attempted to close connection with null server reference" << LL_ENDL;
+ return;
+ }
+
+ LL_INFOS("WebSocket") << "WSConnection closing connection with code " << code
+ << " and reason: " << (reason.empty() ? "(no reason)" : reason) << LL_ENDL;
+
+ if (!mOwningServer.lock()->closeConnection(mConnectionHandle, code, reason))
+ {
+ LL_WARNS("WebSocket") << "Failed to close connection through server" << LL_ENDL;
+ }
+}
+
+bool LLWebsocketMgr::WSConnection::isConnected() const
+{
+ if (mOwningServer.expired())
+ {
+ return false;
+ }
+
+ LLWebsocketMgr::WSServer::ptr_t server = mOwningServer.lock();
+ if (!server)
+ {
+ return false;
+ }
+ return server->getConnectionState(mConnectionHandle) == connection_open;
+}
+
+LLWebsocketMgr::WSConnection::ptr_t LLWebsocketMgr::WSConnection::getSelfPtr()
+{
+ auto server = mOwningServer.lock();
+ if (!server) return nullptr;
+ return server->getConnection(mConnectionHandle);
+}
+
+void LLWebsocketMgr::WSServer::closeAllConnections(U16 code, const std::string& reason)
+{
+ std::vector<connection_h> handles;
+ {
+ LLMutexLock lock(&mConnectionMutex);
+ for (const auto& [handle, conn] : mConnections)
+ {
+ handles.push_back(handle);
+ }
+ }
+ for (const auto& handle : handles)
+ {
+ closeConnection(handle, code, reason);
+ }
+}
diff --git a/indra/llcorehttp/llwebsocketmgr.h b/indra/llcorehttp/llwebsocketmgr.h
new file mode 100644
index 0000000000..19ceb5feed
--- /dev/null
+++ b/indra/llcorehttp/llwebsocketmgr.h
@@ -0,0 +1,328 @@
+/**
+ * @file llwebsocketmgr.h
+ * @brief WebSocket manager singleton for managing WebSocket servers and connections
+ *
+ * $LicenseInfo:firstyear=2025&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2025, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ */
+
+#pragma once
+
+#include "llsingleton.h"
+#include "llsd.h"
+#include "lluuid.h"
+#include "llhost.h"
+#include "llmutex.h"
+
+#include <memory>
+#include <map>
+#include <vector>
+#include <functional>
+#include <thread>
+#include <atomic>
+
+#include <boost/json.hpp>
+
+#include <websocketpp/common/connection_hdl.hpp>
+
+struct Server_impl;
+
+/**
+ * @class LLWebsocketMgr
+ * @brief Singleton manager for WebSocket connections and servers
+ *
+ * This class provides a high-level interface for managing WebSocket connections
+ * and servers using websocketpp library. It handles both client and server
+ * connections, provides thread-safe operations, and integrates with the
+ * existing Linden Lab infrastructure.
+ */
+class LLWebsocketMgr: public LLSingleton<LLWebsocketMgr>
+{
+ LLSINGLETON(LLWebsocketMgr) = default;
+ virtual ~LLWebsocketMgr() = default;
+ LOG_CLASS(LLWebsocketMgr);
+
+public:
+ using connection_h = websocketpp::connection_hdl;
+ class WSServer;
+
+ enum connection_state_t
+ { // must map to websocketpp::session::state
+ connection_connecting = 0,
+ connection_open = 1,
+ connection_closing = 2,
+ connection_closed = 3
+ };
+
+ class WSConnection
+ {
+ friend class LLWebsocketMgr;
+
+ public:
+ using ptr_t = std::shared_ptr<WSConnection>;
+
+ /**
+ * @brief Constructor for WSConnection
+ * @param server Shared pointer to the parent WSServer
+ * @param handle WebSocket connection handle from websocketpp
+ */
+ WSConnection(const std::shared_ptr<WSServer> &server, const connection_h& handle):
+ mConnectionHandle(handle),
+ mOwningServer(server)
+ {}
+
+ virtual ~WSConnection() = default;
+
+ /**
+ * Override this method in derived classes to handle connection establishment.
+ * This is called after the WebSocket handshake is complete and the connection
+ * is ready to send/receive messages.
+ */
+ virtual void onOpen() {}
+
+ /**
+ * Override this method in derived classes to handle connection closure.
+ * This is called when the connection has been terminated, either normally
+ * or due to an error condition.
+ */
+ virtual void onClose() {}
+
+ /**
+ * @brief Called when a message is received
+ * @param message The received message as a string
+ *
+ * Override this method in derived classes to handle incoming messages.
+ * Currently only text messages are supported.
+ */
+ virtual void onMessage(const std::string& message) {}
+
+ /**
+ * @brief Send a message to the connected client
+ * @param message The message string to send
+ * @return true if the message was queued successfully, false on error
+ *
+ * Sends a text message to the remote endpoint. The message is queued
+ * asynchronously and may not be sent immediately.
+ */
+ virtual bool sendMessage(const std::string& message) const;
+ bool sendMessage(const boost::json::value& json) const;
+ bool sendMessage(const LLSD& data) const;
+
+ /**
+ * @brief Close the WebSocket connection gracefully
+ * @param code Optional close code (default: normal closure)
+ * @param reason Optional reason string (default: empty)
+ *
+ * Initiates a graceful WebSocket close handshake. The connection will
+ * send a close frame with the specified code and reason, then wait for
+ * the remote endpoint to respond with its own close frame before
+ * actually closing the underlying TCP connection.
+ *
+ * Common close codes:
+ * - 1000: Normal closure (default)
+ * - 1001: Going away (server shutting down, page navigating away)
+ * - 1002: Protocol error
+ * - 1003: Unsupported data type
+ * - 1008: Policy violation
+ * - 1009: Message too big
+ *
+ * @note After calling this method, no further messages should be sent
+ * @note The onClose() callback will be invoked when the close handshake completes
+ */
+ void closeConnection(U16 code = 1000, const std::string& reason = std::string());
+
+ bool isConnected() const;
+
+ protected:
+ /// Returns a shared_ptr to this connection, retrieved from the owning server.
+ /// Valid only while the connection is open and registered with the server.
+ ptr_t getSelfPtr();
+
+ connection_h mConnectionHandle;
+ std::weak_ptr<WSServer> mOwningServer; // Back-reference to the server this connection belongs to
+ };
+
+ /**
+ * @class WSServer
+ * @brief Base class for WebSocket servers with customizable connection handling
+ *
+ * WSServer provides a high-level abstraction over websocketpp servers, handling
+ * threading, connection management, and event dispatching. Derive from this class
+ * to create custom WebSocket servers with application-specific logic.
+ *
+ * ## Basic Usage
+ *
+ * @code
+ * class MyServer : public LLWebsocketMgr::WSServer
+ * {
+ * public:
+ * MyServer(const std::string& name, U16 port)
+ * : WSServer(name, port, false) // Listen on all interfaces
+ * {}
+ *
+ * void onConnectionOpened(const WSConnection::ptr_t& connection) override
+ * {
+ * LL_INFOS("MyServer") << "New client connected" << LL_ENDL;
+ * // Send welcome message
+ * connection->sendMessage("Welcome to the server!");
+ * }
+ *
+ * void onConnectionClosed(const WSConnection::ptr_t& connection) override
+ * {
+ * LL_INFOS("MyServer") << "Client disconnected" << LL_ENDL;
+ * }
+ *
+ * protected:
+ * // Use custom connection class
+ * WSConnection::ptr_t connectionFactory(WSServer::ptr_t server, connection_h handle) override
+ * {
+ * return std::make_shared<MyConnection>(server, handle);
+ * }
+ * };
+ * @endcode
+ *
+ * ## Connection Management
+ *
+ * The server automatically manages connection lifetimes and provides several ways
+ * to interact with connections:
+ *
+ * - `broadcastMessage()` - Send message to all connected clients
+ * - `sendMessageTo()` - Send message to specific connection
+ * - `closeConnection()` - Close specific connection with code/reason
+ * - `getConnection()` - Get connection object by handle
+ *
+ * ## Thread Safety
+ *
+ * All public methods are thread-safe and can be called from any thread. The server
+ * runs its own background thread for handling WebSocket events, while connection
+ * callbacks are also executed on this background thread.
+ */
+ class WSServer: public std::enable_shared_from_this<WSServer>
+ {
+ friend struct Server_impl;
+ friend class WSConnection;
+ friend class LLWebsocketMgr;
+
+ public:
+ using ptr_t = std::shared_ptr<WSServer>;
+
+ WSServer(std::string_view name, U16 port, bool local_only = true);
+ virtual ~WSServer();
+
+ virtual void onStarted() {}
+ virtual void onStopped() {}
+
+ virtual void onConnectionOpened(const WSConnection::ptr_t& connection) { }
+ virtual void onConnectionClosed(const WSConnection::ptr_t& connection) { }
+
+ bool isRunning() const;
+ size_t getConnectionCount() const
+ {
+ LLMutexLock lock(&mConnectionMutex);
+ return mConnections.size();
+ }
+
+ void broadcastMessage(const std::string& message);
+ virtual bool update() { return true; }
+
+ /**
+ * @brief Whether an upgrade request from this origin is let in
+ * @param origin The request's Origin header, empty where it had none
+ *
+ * A browser sends the page's origin with every WebSocket it opens,
+ * and nothing stops a page -- or media on a prim, in the viewer's
+ * own browser -- from opening one to 127.0.0.1. A program, such as
+ * an editor's extension, sends none. By default only those are let
+ * in; a request refused is answered 403 and never opens. Called on
+ * the server's thread.
+ */
+ virtual bool acceptOrigin(const std::string& origin) const { return origin.empty(); }
+
+ connection_state_t getConnectionState(const connection_h& handle) const;
+
+ protected:
+ /// Snapshot of live connections. Safe to iterate after the lock is
+ /// released -- these are real shared_ptrs, not weak references.
+ std::vector<WSConnection::ptr_t> getConnections() const;
+
+ virtual WSConnection::ptr_t connectionFactory(WSServer::ptr_t server, connection_h handle);
+
+ bool start();
+ void stop();
+
+ bool sendMessageTo(const connection_h& handle, const std::string& message);
+
+ /**
+ * @brief Close a specific connection gracefully
+ * @param handle The connection handle to close
+ * @param code Close code (default: normal closure)
+ * @param reason Close reason string (default: empty)
+ * @return true if close was initiated successfully, false on error
+ *
+ * Internal method used by WSConnection to close individual connections.
+ * This method is thread-safe and can be called from any thread.
+ */
+ bool closeConnection(const connection_h& handle, U16 code = 1000, const std::string& reason = std::string());
+ void closeAllConnections(U16 code = 1001, const std::string& reason = "Server shutting down");
+
+ private:
+ using connection_map_t = std::map<connection_h, WSConnection::ptr_t, std::owner_less<connection_h> >;
+
+ WSConnection::ptr_t getConnection(const connection_h& handle);
+
+ void handleOpenConnection(const connection_h& handle);
+ void handleCloseConnection(const connection_h& handle);
+ void handleMessage(const connection_h& handle, const std::string& message);
+
+ std::string mServerName;
+ std::unique_ptr<Server_impl> mImpl;
+ connection_map_t mConnections;
+ mutable LLMutex mConnectionMutex;
+
+ // Threading support
+ std::thread mServerThread; ///< Thread running the ASIO event loop
+ std::atomic<bool> mShouldStop{ false }; ///< Thread-safe stop flag
+ std::atomic<bool> mRunning{ false }; ///< True from just before the I/O thread launches until it exits
+ mutable LLMutex mThreadMutex; ///< Mutex for thread synchronization
+ };
+
+ // Server and Connection Management
+ WSServer::ptr_t findServerByName(const std::string &name) const;
+
+ bool addServer(const WSServer::ptr_t& server);
+ bool removeServer(const std::string &name);
+
+ bool startServer(const std::string &name) const;
+ void stopServer(const std::string &name) const;
+
+ void update();
+
+protected:
+ void initSingleton() override;
+ void cleanupSingleton() override;
+
+private:
+ using server_map_t = std::map<std::string, WSServer::ptr_t>;
+
+ void stopAllServers();
+
+ server_map_t mServers;
+};
diff --git a/indra/llcorehttp/tests/jsonrpcws_test.cpp b/indra/llcorehttp/tests/jsonrpcws_test.cpp
new file mode 100644
index 0000000000..c07cb6f4fd
--- /dev/null
+++ b/indra/llcorehttp/tests/jsonrpcws_test.cpp
@@ -0,0 +1,553 @@
+/**
+ * @file jsonrpcws_test.cpp
+ * @brief Security-focused unit tests for LLJSONRPCConnection / LLJSONRPCServer
+ *
+ * $LicenseInfo:firstyear=2026&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2026, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ */
+
+#include "linden_common.h"
+
+#include "lljsonrpcws.h"
+#include "llsdjson.h"
+
+#include "../test/lltut.h"
+
+#include <boost/json.hpp>
+#include <vector>
+
+namespace
+{
+ // Exposes the protected test-only members LLJSONRPCConnection provides
+ // for exactly this purpose, on a connection with no real transport.
+ class TestJSONRPCConnection : public LLJSONRPCConnection
+ {
+ public:
+ TestJSONRPCConnection()
+ : LLJSONRPCConnection(LLWebsocketMgr::WSServer::ptr_t(), LLWebsocketMgr::connection_h())
+ {
+ }
+
+ using LLJSONRPCConnection::processMessage;
+ using LLJSONRPCConnection::validateMessage;
+ using LLJSONRPCConnection::testInjectPendingRequest;
+ using LLJSONRPCConnection::testPendingRequestCount;
+ using LLJSONRPCConnection::testSweepTimeouts;
+ using LLJSONRPCConnection::setAuthenticated;
+ };
+
+ // Minimal concrete server, used only to register the built-in
+ // system.* methods on a connection via setupConnectionMethods() --
+ // LLJSONRPCServer itself is abstract (handleGetVersion is pure virtual).
+ // Never started, so port 0 is fine.
+ class TestJSONRPCServer : public LLJSONRPCServer
+ {
+ public:
+ TestJSONRPCServer() : LLJSONRPCServer("test_jsonrpcws_server", 0, true) {}
+
+ using LLJSONRPCServer::setupConnectionMethods;
+
+ LLSD handleGetVersion(const LLJSONRPCConnection::ptr_t&, const LLSD&) const override
+ {
+ return LLSD();
+ }
+ };
+
+ // Captures what would be sent to the peer, since there is no real
+ // transport to observe it on.
+ class CapturingJSONRPCConnection : public TestJSONRPCConnection
+ {
+ public:
+ mutable std::vector<std::string> mSent;
+
+ bool sendMessage(const std::string& message) const override
+ {
+ mSent.push_back(message);
+ return true;
+ }
+ };
+}
+
+namespace tut
+{
+ struct jsonrpcws_data
+ {
+ };
+
+ typedef test_group<jsonrpcws_data> jsonrpcws_test;
+ typedef jsonrpcws_test::object jsonrpcws_object;
+ jsonrpcws_test jsonrpcws_testcase("LLJSONRPCConnection");
+
+ template<> template<>
+ void jsonrpcws_object::test<1>()
+ {
+ set_test_name("an unauthenticated connection dispatches neither requests nor notifications");
+
+ TestJSONRPCConnection conn;
+ conn.setAuthenticated(false);
+
+ int calls = 0;
+ conn.registerMethod("object.list",
+ [&](const std::string&, const LLSD&, const LLSD&) -> LLSD
+ {
+ ++calls;
+ return LLSD();
+ });
+
+ const LLSD request = LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "object.list", LLSD::emptyMap(), LLSD(), LLSD());
+ conn.processMessage(request);
+
+ const LLSD notification = LLJSONRPCConnection::makeEnvelope(LLSD(), "object.list", LLSD::emptyMap(), LLSD(), LLSD());
+ conn.processMessage(notification);
+
+ ensure_equals("neither a request nor a notification reaches the handler", calls, 0);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<2>()
+ {
+ set_test_name("once authenticated, requests and notifications dispatch normally");
+
+ TestJSONRPCConnection conn;
+ conn.setAuthenticated(true);
+ ensure("authenticated after the peer proves itself", conn.isAuthenticated());
+
+ int calls = 0;
+ conn.registerMethod("object.list",
+ [&](const std::string&, const LLSD&, const LLSD&) -> LLSD
+ {
+ ++calls;
+ return LLSD();
+ });
+
+ const LLSD request = LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "object.list", LLSD::emptyMap(), LLSD(), LLSD());
+ conn.processMessage(request);
+ ensure_equals("the request reaches the handler", calls, 1);
+
+ const LLSD notification = LLJSONRPCConnection::makeEnvelope(LLSD(), "object.list", LLSD::emptyMap(), LLSD(), LLSD());
+ conn.processMessage(notification);
+ ensure_equals("the notification also reaches the handler", calls, 2);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<3>()
+ {
+ set_test_name("an unauthenticated connection still receives the answers to its own calls");
+
+ TestJSONRPCConnection conn;
+ ensure("unauthenticated by default", !conn.isAuthenticated());
+
+ LLSD answer;
+ conn.testInjectPendingRequest(
+ "rpc_1",
+ LLTimer::getTotalSeconds() + 60.0,
+ [&](const LLSD& result, const LLSD& error)
+ {
+ ensure("no error", error.isUndefined());
+ answer = result;
+ });
+
+ LLSD result;
+ result["challenge_response"] = "secret";
+ conn.processMessage(LLJSONRPCConnection::makeEnvelope(LLSD("rpc_1"), std::string(), LLSD(), result, LLSD()));
+ ensure_equals("the answer reached the caller", answer["challenge_response"].asString(), std::string("secret"));
+ ensure("and it is answered", conn.testPendingRequestCount() == 0);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<4>()
+ {
+ set_test_name("an unauthenticated request is answered -32002; a notification gets nothing back");
+
+ CapturingJSONRPCConnection conn;
+ ensure("unauthenticated by default", !conn.isAuthenticated());
+
+ const LLSD request = LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "object.list", LLSD::emptyMap(), LLSD(), LLSD());
+ conn.processMessage(request);
+ ensure_equals("exactly one message sent for the request", conn.mSent.size(), (size_t)1);
+
+ boost::system::error_code ec;
+ const LLSD sent = LlsdFromJson(boost::json::parse(conn.mSent[0], ec));
+ ensure("parsed without error", !ec);
+ ensure_equals("unauthorized error code", sent["error"]["code"].asInteger(), LLJSONRPCConnection::RPCError::UNAUTHORIZED);
+
+ const LLSD notification = LLJSONRPCConnection::makeEnvelope(LLSD(), "object.list", LLSD::emptyMap(), LLSD(), LLSD());
+ conn.processMessage(notification);
+ ensure_equals("nothing sent for a notification", conn.mSent.size(), (size_t)1);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<5>()
+ {
+ set_test_name("malformed JSON is answered with a parse error, and the connection survives");
+
+ CapturingJSONRPCConnection conn;
+ conn.setAuthenticated(true);
+
+ conn.onMessage("{not valid json");
+ ensure_equals("a parse error is sent", conn.mSent.size(), (size_t)1);
+
+ boost::system::error_code ec;
+ const LLSD sent = LlsdFromJson(boost::json::parse(conn.mSent[0], ec));
+ ensure("parsed without error", !ec);
+ ensure_equals("parse error code", sent["error"]["code"].asInteger(), LLJSONRPCConnection::RPCError::PARSE_ERROR);
+
+ // The connection itself is unaffected: valid follow-up traffic still works.
+ int calls = 0;
+ conn.registerMethod("object.list",
+ [&](const std::string&, const LLSD&, const LLSD&) -> LLSD
+ {
+ ++calls;
+ return LLSD();
+ });
+ conn.onMessage(boost::json::serialize(LlsdToJson(LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "object.list", LLSD::emptyMap(), LLSD(), LLSD()))));
+ ensure_equals("a later valid request still dispatches", calls, 1);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<6>()
+ {
+ set_test_name("a batch request is rejected outright; batch support is intentionally disabled");
+
+ CapturingJSONRPCConnection conn;
+ conn.setAuthenticated(true);
+
+ const LLSD one = LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "object.list", LLSD::emptyMap(), LLSD(), LLSD());
+ LLSD batch = LLSD::emptyArray();
+ batch.append(one);
+ batch.append(one);
+
+ conn.onMessage(boost::json::serialize(LlsdToJson(batch)));
+ ensure_equals("a single error is sent for the whole batch", conn.mSent.size(), (size_t)1);
+
+ boost::system::error_code ec;
+ const LLSD sent = LlsdFromJson(boost::json::parse(conn.mSent[0], ec));
+ ensure("parsed without error", !ec);
+ ensure_equals("invalid request error code", sent["error"]["code"].asInteger(), LLJSONRPCConnection::RPCError::INVALID_REQUEST);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<7>()
+ {
+ set_test_name("a missing or wrong jsonrpc version is rejected by validateMessage");
+
+ TestJSONRPCConnection conn;
+
+ LLSD missing_version;
+ missing_version["method"] = "object.list";
+ ensure("missing version fails validation", !conn.validateMessage(missing_version, true));
+
+ LLSD wrong_version = missing_version;
+ wrong_version["jsonrpc"] = "1.0";
+ ensure("wrong version fails validation", !conn.validateMessage(wrong_version, true));
+
+ LLSD correct_version = missing_version;
+ correct_version["jsonrpc"] = "2.0";
+ ensure("correct version passes validation", conn.validateMessage(correct_version, true));
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<8>()
+ {
+ set_test_name("params of the wrong type are rejected; the handler is never invoked");
+
+ TestJSONRPCConnection conn;
+ conn.setAuthenticated(true);
+
+ LLSD bad_params;
+ bad_params["jsonrpc"] = "2.0";
+ bad_params["method"] = "object.list";
+ bad_params["params"] = "not-an-array-or-object";
+ ensure("string params fail validation", !conn.validateMessage(bad_params, true));
+
+ int calls = 0;
+ conn.registerMethod("object.list",
+ [&](const std::string&, const LLSD&, const LLSD&) -> LLSD
+ {
+ ++calls;
+ return LLSD();
+ });
+
+ conn.processMessage(bad_params);
+ ensure_equals("the handler is never invoked", calls, 0);
+
+ LLSD good_params = bad_params;
+ good_params["params"] = LLSD::emptyMap();
+ conn.processMessage(good_params);
+ ensure_equals("valid params do dispatch", calls, 1);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<9>()
+ {
+ set_test_name("a response for an unknown id is silently ignored");
+
+ TestJSONRPCConnection conn;
+
+ bool callback_called = false;
+ conn.testInjectPendingRequest(
+ "req_1",
+ LLTimer::getTotalSeconds() + 60.0,
+ [&](const LLSD&, const LLSD&)
+ {
+ callback_called = true;
+ });
+
+ LLSD result;
+ result["value"] = "ok";
+ conn.processMessage(LLJSONRPCConnection::makeEnvelope(LLSD("unknown_id"), std::string(), LLSD(), result, LLSD()));
+
+ ensure("no callback fired for an unrelated id", !callback_called);
+ ensure_equals("the real pending request is still tracked", conn.testPendingRequestCount(), (size_t)1);
+
+ conn.processMessage(LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), std::string(), LLSD(), result, LLSD()));
+ ensure("the callback fires for its own id", callback_called);
+ ensure_equals("and it is removed once answered", conn.testPendingRequestCount(), (size_t)0);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<10>()
+ {
+ set_test_name("a response with both result and error, or with neither, is rejected as invalid");
+
+ TestJSONRPCConnection conn;
+
+ LLSD both;
+ both["jsonrpc"] = "2.0";
+ both["id"] = "req_1";
+ both["result"] = "ok";
+ both["error"]["code"] = -1;
+ both["error"]["message"] = "bad";
+ ensure("result and error together fail validation", !conn.validateMessage(both, false));
+
+ LLSD neither;
+ neither["jsonrpc"] = "2.0";
+ neither["id"] = "req_1";
+ ensure("neither result nor error fails validation", !conn.validateMessage(neither, false));
+
+ LLSD result_only;
+ result_only["jsonrpc"] = "2.0";
+ result_only["id"] = "req_1";
+ result_only["result"] = "ok";
+ ensure("result alone passes validation", conn.validateMessage(result_only, false));
+
+ bool callback_called = false;
+ conn.testInjectPendingRequest(
+ "req_1",
+ LLTimer::getTotalSeconds() + 60.0,
+ [&](const LLSD&, const LLSD&)
+ {
+ callback_called = true;
+ });
+
+ conn.processMessage(both);
+ ensure("an invalid response never reaches the callback", !callback_called);
+ ensure_equals("the pending request is still tracked", conn.testPendingRequestCount(), (size_t)1);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<11>()
+ {
+ set_test_name("an unauthenticated peer is told unauthorized, never that the method doesn't exist");
+
+ CapturingJSONRPCConnection conn;
+
+ const LLSD request = LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "no.such.method", LLSD::emptyMap(), LLSD(), LLSD());
+ conn.processMessage(request);
+ ensure_equals("one message sent", conn.mSent.size(), (size_t)1);
+
+ boost::system::error_code ec;
+ LLSD sent = LlsdFromJson(boost::json::parse(conn.mSent[0], ec));
+ ensure("parsed without error", !ec);
+ ensure_equals("unauthorized, not method-not-found", sent["error"]["code"].asInteger(), LLJSONRPCConnection::RPCError::UNAUTHORIZED);
+
+ conn.setAuthenticated(true);
+ conn.processMessage(request);
+ ensure_equals("a second message sent once authenticated", conn.mSent.size(), (size_t)2);
+
+ sent = LlsdFromJson(boost::json::parse(conn.mSent[1], ec));
+ ensure("parsed without error", !ec);
+ ensure_equals("now it is method-not-found", sent["error"]["code"].asInteger(), LLJSONRPCConnection::RPCError::METHOD_NOT_FOUND);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<12>()
+ {
+ set_test_name("unregisterMethod removes both sync and async handlers");
+
+ TestJSONRPCConnection conn;
+
+ conn.registerMethod("sync.method",
+ [](const std::string&, const LLSD&, const LLSD&) -> LLSD
+ {
+ return LLSD();
+ });
+ conn.registerAsyncMethod("async.method",
+ [](const std::string&, const LLSD&, const LLSD&) -> LLSD
+ {
+ return LLSD();
+ });
+
+ ensure_equals("sync method registered", conn.getMethods().count("sync.method"), (size_t)1);
+ ensure_equals("async method registered", conn.getMethods().count("async.method"), (size_t)1);
+
+ conn.unregisterMethod("sync.method");
+ conn.unregisterMethod("async.method");
+
+ ensure_equals("sync method gone", conn.getMethods().count("sync.method"), (size_t)0);
+ ensure_equals("async method gone", conn.getMethods().count("async.method"), (size_t)0);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<13>()
+ {
+ set_test_name("a handler throwing a plain std::exception is converted to a generic internal error");
+
+ CapturingJSONRPCConnection conn;
+ conn.setAuthenticated(true);
+
+ conn.registerMethod("boom",
+ [](const std::string&, const LLSD&, const LLSD&) -> LLSD
+ {
+ throw std::runtime_error("boom detail");
+ });
+
+ conn.processMessage(LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "boom", LLSD::emptyMap(), LLSD(), LLSD()));
+ ensure_equals("one message sent", conn.mSent.size(), (size_t)1);
+
+ boost::system::error_code ec;
+ const LLSD sent = LlsdFromJson(boost::json::parse(conn.mSent[0], ec));
+ ensure("parsed without error", !ec);
+ ensure_equals("internal error code", sent["error"]["code"].asInteger(), LLJSONRPCConnection::RPCError::INTERNAL_ERROR);
+
+ // The exception's own message must never reach the client -- it can
+ // carry paths, connection strings, or other internal detail.
+ ensure("the exception's own message is not forwarded to the client",
+ sent["error"]["message"].asString().find("boom detail") == std::string::npos);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<14>()
+ {
+ set_test_name("call() with no way to send returns undefined and leaves nothing pending");
+
+ TestJSONRPCConnection conn;
+
+ bool callback_called = false;
+ const LLSD id = conn.call("object.list", LLSD::emptyMap(),
+ [&](const LLSD&, const LLSD&)
+ {
+ callback_called = true;
+ });
+
+ ensure("call() reports it could not send", id.isUndefined());
+ ensure_equals("nothing left pending", conn.testPendingRequestCount(), (size_t)0);
+ ensure("the callback is never invoked for an unsent call", !callback_called);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<15>()
+ {
+ set_test_name("the built-in system.* methods are gated by authentication like any other method");
+
+ TestJSONRPCServer server;
+ auto conn = std::make_shared<CapturingJSONRPCConnection>();
+ server.setupConnectionMethods(conn);
+
+ ensure_equals("system.listMethods is registered", conn->getMethods().count("system.listMethods"), (size_t)1);
+ ensure_equals("system.getStats is registered", conn->getMethods().count("system.getStats"), (size_t)1);
+
+ conn->processMessage(LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "system.listMethods", LLSD::emptyMap(), LLSD(), LLSD()));
+ ensure_equals("one message sent", conn->mSent.size(), (size_t)1);
+
+ boost::system::error_code ec;
+ const LLSD sent = LlsdFromJson(boost::json::parse(conn->mSent[0], ec));
+ ensure("parsed without error", !ec);
+ ensure_equals("unauthenticated, even a built-in method is refused", sent["error"]["code"].asInteger(), LLJSONRPCConnection::RPCError::UNAUTHORIZED);
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<16>()
+ {
+ set_test_name("makeEnvelope includes id and method by the documented rules");
+
+ // Notification: id undefined, method non-empty -> id omitted.
+ const LLSD notification = LLJSONRPCConnection::makeEnvelope(LLSD(), "object.list", LLSD::emptyMap(), LLSD(), LLSD());
+ ensure("notification omits id", !notification.has("id"));
+ ensure_equals("notification keeps method", notification["method"].asString(), std::string("object.list"));
+
+ // Request: id defined, method non-empty -> both kept.
+ const LLSD request = LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "object.list", LLSD::emptyMap(), LLSD(), LLSD());
+ ensure("request keeps id", request.has("id"));
+ ensure_equals("request keeps method", request["method"].asString(), std::string("object.list"));
+
+ // Success response: id defined, method empty -> id kept, method omitted.
+ const LLSD response = LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), std::string(), LLSD(), LLSD("ok"), LLSD());
+ ensure("response keeps id", response.has("id"));
+ ensure("response omits method", !response.has("method"));
+ ensure_equals("response keeps result", response["result"].asString(), std::string("ok"));
+
+ // Error response with an undefined id (e.g. a parse error): method
+ // is empty, so id is kept even though undefined -- serializes as null.
+ LLSD error_obj;
+ error_obj["code"] = -32700;
+ error_obj["message"] = "Parse error";
+ const LLSD error_response = LLJSONRPCConnection::makeEnvelope(LLSD(), std::string(), LLSD(), LLSD(), error_obj);
+ ensure("error response keeps id even when undefined", error_response.has("id"));
+ ensure("that id is null", error_response["id"].isUndefined());
+ ensure("error response omits method", !error_response.has("method"));
+ }
+
+ template<> template<>
+ void jsonrpcws_object::test<17>()
+ {
+ set_test_name("onClose fails every pending request with CONNECTION_CLOSED, exactly once");
+
+ TestJSONRPCConnection conn;
+
+ int callback_count_a = 0;
+ int callback_count_b = 0;
+ conn.testInjectPendingRequest(
+ "req_a",
+ LLTimer::getTotalSeconds() + 60.0,
+ [&](const LLSD& result, const LLSD& error)
+ {
+ ++callback_count_a;
+ ensure("no result on connection close", result.isUndefined());
+ ensure_equals("connection closed error code", error["code"].asInteger(), LLJSONRPCConnection::RPCError::CONNECTION_CLOSED);
+ });
+ conn.testInjectPendingRequest(
+ "req_b",
+ LLTimer::getTotalSeconds() + 60.0,
+ [&](const LLSD&, const LLSD& error)
+ {
+ ++callback_count_b;
+ ensure_equals("connection closed error code", error["code"].asInteger(), LLJSONRPCConnection::RPCError::CONNECTION_CLOSED);
+ });
+ ensure_equals("two requests pending", conn.testPendingRequestCount(), (size_t)2);
+
+ conn.onClose();
+
+ ensure_equals("first callback fires exactly once", callback_count_a, 1);
+ ensure_equals("second callback fires exactly once", callback_count_b, 1);
+ ensure_equals("nothing left pending", conn.testPendingRequestCount(), (size_t)0);
+ }
+}
diff --git a/indra/llcorehttp/tests/llcorehttp_test.cpp b/indra/llcorehttp/tests/llcorehttp_test.cpp
index c0cc2c8030..cc3c241a96 100755..100644
--- a/indra/llcorehttp/tests/llcorehttp_test.cpp
+++ b/indra/llcorehttp/tests/llcorehttp_test.cpp
@@ -41,21 +41,16 @@
#include "test_httpstatus.hpp"
#include "test_refcounted.hpp"
#include "test_httpoperation.hpp"
-// As of 2019-06-28, test_httprequest.hpp consistently crashes on Mac Release
-// builds for reasons not yet diagnosed.
-#if ! (LL_DARWIN && LL_RELEASE)
#include "test_httprequest.hpp"
-#endif
#include "test_httpheaders.hpp"
#include "test_httprequestqueue.hpp"
+#include "test_jsonrpcws.hpp"
+#include "test_websocketmgr.hpp"
#include "_httpservice.h"
#include "llproxy.h"
#include "llcleanup.h"
-void ssl_thread_id_callback(CRYPTO_THREADID*);
-void ssl_locking_callback(int mode, int type, const char * file, int line);
-
#if 0 // lltut provides main and runner
namespace tut
@@ -80,27 +75,10 @@ int main()
#endif // 0
-int ssl_mutex_count(0);
-LLCoreInt::HttpMutex ** ssl_mutex_list = NULL;
-
void init_curl()
{
curl_global_init(CURL_GLOBAL_ALL);
- ssl_mutex_count = CRYPTO_num_locks();
- if (ssl_mutex_count > 0)
- {
- ssl_mutex_list = new LLCoreInt::HttpMutex * [ssl_mutex_count];
-
- for (int i(0); i < ssl_mutex_count; ++i)
- {
- ssl_mutex_list[i] = new LLCoreInt::HttpMutex;
- }
-
- CRYPTO_set_locking_callback(ssl_locking_callback);
- CRYPTO_THREADID_set_callback(ssl_thread_id_callback);
- }
-
LLProxy::getInstance();
}
@@ -108,39 +86,6 @@ void init_curl()
void term_curl()
{
SUBSYSTEM_CLEANUP(LLProxy);
-
- CRYPTO_set_locking_callback(NULL);
- for (int i(0); i < ssl_mutex_count; ++i)
- {
- delete ssl_mutex_list[i];
- }
- delete [] ssl_mutex_list;
-}
-
-
-void ssl_thread_id_callback(CRYPTO_THREADID* pthreadid)
-{
-#if defined(WIN32)
- CRYPTO_THREADID_set_pointer(pthreadid, GetCurrentThread());
-#else
- CRYPTO_THREADID_set_pointer(pthreadid, pthread_self());
-#endif
-}
-
-
-void ssl_locking_callback(int mode, int type, const char * /* file */, int /* line */)
-{
- if (type >= 0 && type < ssl_mutex_count)
- {
- if (mode & CRYPTO_LOCK)
- {
- ssl_mutex_list[type]->lock();
- }
- else
- {
- ssl_mutex_list[type]->unlock();
- }
- }
}
diff --git a/indra/llcorehttp/tests/test_httprequest.hpp b/indra/llcorehttp/tests/test_httprequest.hpp
index aed906bb8f..68f8c4f71f 100644
--- a/indra/llcorehttp/tests/test_httprequest.hpp
+++ b/indra/llcorehttp/tests/test_httprequest.hpp
@@ -454,6 +454,10 @@ void HttpRequestTestObjectType::test<4>()
template <> template <>
void HttpRequestTestObjectType::test<5>()
{
+#ifndef LL_WINDOWS
+ skip("Skip due to issues with testing pthread cancellation");
+#endif
+
ScopedCurlInit ready;
set_test_name("HttpRequest Spin (soft) + NoOp + hard termination");
@@ -517,6 +521,9 @@ void HttpRequestTestObjectType::test<5>()
template <> template <>
void HttpRequestTestObjectType::test<6>()
{
+#ifndef LL_WINDOWS
+ skip("Skip due to issues with testing pthread cancellation");
+#endif
ScopedCurlInit ready;
set_test_name("HttpRequest Spin + NoOp + hard termination");
@@ -2737,13 +2744,6 @@ void HttpRequestTestObjectType::test<22>()
set_test_name("BUG-2295");
-#if LL_WINDOWS && ADDRESS_SIZE == 64
- // teamcity win64 builds freeze on this test, if you figure out the cause, please fix it
- if (getenv("TEAMCITY_PROJECT_NAME"))
- {
- skip("BUG-2295 - partial load on W64 causes freeze");
- }
-#endif
// Handler can be stack-allocated *if* there are no dangling
// references to it after completion of this method.
// Create before memory record as the string copy will bump numbers.
@@ -2779,7 +2779,7 @@ void HttpRequestTestObjectType::test<22>()
for (int i(0); i < test_count; ++i)
{
char buffer[128];
- sprintf(buffer, "/bug2295/%d/", i);
+ snprintf(buffer, sizeof(buffer), "/bug2295/%d/", i);
HttpHandle handle = req->requestGetByteRange(HttpRequest::DEFAULT_POLICY_ID,
url_base + buffer,
0,
@@ -2810,7 +2810,7 @@ void HttpRequestTestObjectType::test<22>()
for (int i(0); i < test2_count; ++i)
{
char buffer[128];
- sprintf(buffer, "/bug2295/00000012/%d/", i);
+ snprintf(buffer, sizeof(buffer), "/bug2295/00000012/%d/", i);
HttpHandle handle = req->requestGetByteRange(HttpRequest::DEFAULT_POLICY_ID,
url_base + buffer,
0,
@@ -2841,7 +2841,7 @@ void HttpRequestTestObjectType::test<22>()
for (int i(0); i < test3_count; ++i)
{
char buffer[128];
- sprintf(buffer, "/bug2295/inv_cont_range/%d/", i);
+ snprintf(buffer, sizeof(buffer), "/bug2295/inv_cont_range/%d/", i);
HttpHandle handle = req->requestGetByteRange(HttpRequest::DEFAULT_POLICY_ID,
url_base + buffer,
0,
@@ -2917,14 +2917,6 @@ void HttpRequestTestObjectType::test<23>()
set_test_name("HttpRequest GET 503s with 'Retry-After'");
-#if LL_WINDOWS && ADDRESS_SIZE == 64
- // teamcity win64 builds freeze on this test, if you figure out the cause, please fix it
- if (getenv("TEAMCITY_PROJECT_NAME"))
- {
- skip("llcorehttp 503-with-retry test hangs on Windows 64");
- }
-#endif
-
// This tests mainly that the code doesn't fall over if
// various well- and mis-formed Retry-After headers are
// sent along with the response. Direct inspection of
diff --git a/indra/llcorehttp/tests/test_jsonrpcws.hpp b/indra/llcorehttp/tests/test_jsonrpcws.hpp
new file mode 100644
index 0000000000..a40d0117b8
--- /dev/null
+++ b/indra/llcorehttp/tests/test_jsonrpcws.hpp
@@ -0,0 +1,203 @@
+/**
+ * @file test_jsonrpcws.hpp
+ * @brief unit tests for LLJSONRPCConnection helpers and dispatch behavior
+ */
+
+#ifndef TEST_LLCORE_JSONRPCWS_H_
+#define TEST_LLCORE_JSONRPCWS_H_
+
+#include "lljsonrpcws.h"
+#include "lltimer.h"
+
+namespace
+{
+class TestJSONRPCConnection : public LLJSONRPCConnection
+{
+public:
+ TestJSONRPCConnection()
+ : LLJSONRPCConnection(LLWebsocketMgr::WSServer::ptr_t(), LLWebsocketMgr::connection_h())
+ {
+ }
+
+ using LLJSONRPCConnection::processMessage;
+ using LLJSONRPCConnection::validateMessage;
+ using LLJSONRPCConnection::testInjectPendingRequest;
+ using LLJSONRPCConnection::testPendingRequestCount;
+ using LLJSONRPCConnection::testSweepTimeouts;
+ using LLJSONRPCConnection::setAuthenticated;
+};
+}
+
+namespace tut
+{
+ struct JSONRPCWSTestData
+ {
+ };
+
+ typedef test_group<JSONRPCWSTestData> JSONRPCWSTestGroupType;
+ typedef JSONRPCWSTestGroupType::object JSONRPCWSTestObjectType;
+ JSONRPCWSTestGroupType JSONRPCWSTestGroup("LLJSONRPCConnection Tests");
+
+ template<> template<>
+ void JSONRPCWSTestObjectType::test<1>()
+ {
+ set_test_name("makeEnvelope notification omits id");
+
+ LLSD params;
+ params["value"] = 42;
+ LLSD env = LLJSONRPCConnection::makeEnvelope(LLSD(), "runtime.debug", params, LLSD(), LLSD());
+
+ ensure("jsonrpc field should exist", env.has("jsonrpc"));
+ ensure_equals("jsonrpc version", env["jsonrpc"].asString(), "2.0");
+ ensure("notification should omit id", !env.has("id"));
+ ensure_equals("method", env["method"].asString(), "runtime.debug");
+ ensure_equals("param round trip", env["params"]["value"].asInteger(), 42);
+ }
+
+ template<> template<>
+ void JSONRPCWSTestObjectType::test<2>()
+ {
+ set_test_name("makeEnvelope response keeps id slot");
+
+ LLSD env = LLJSONRPCConnection::makeEnvelope(LLSD(), std::string(), LLSD(), LLSD("ok"), LLSD());
+
+ ensure("response should include id", env.has("id"));
+ ensure("response should not include method", !env.has("method"));
+ ensure_equals("result", env["result"].asString(), "ok");
+ }
+
+ template<> template<>
+ void JSONRPCWSTestObjectType::test<3>()
+ {
+ set_test_name("validateMessage accepts valid request and rejects invalid params");
+
+ TestJSONRPCConnection conn;
+
+ LLSD valid;
+ valid["jsonrpc"] = "2.0";
+ valid["method"] = "session.ping";
+ LLSD params = LLSD::emptyMap();
+ params["timestamp"] = 123;
+ valid["params"] = params;
+ ensure("valid request should pass", conn.validateMessage(valid, true));
+
+ LLSD invalid = valid;
+ invalid["params"] = "not-an-array-or-object";
+ ensure("invalid params type should fail", !conn.validateMessage(invalid, true));
+ }
+
+ template<> template<>
+ void JSONRPCWSTestObjectType::test<4>()
+ {
+ set_test_name("processMessage dispatches notification handler");
+
+ TestJSONRPCConnection conn;
+ conn.setAuthenticated(true);
+
+ bool called = false;
+ LLSD seen_id;
+ LLSD seen_params;
+ conn.registerMethod("runtime.debug",
+ [&](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD
+ {
+ called = true;
+ ensure_equals("method propagated", method, "runtime.debug");
+ seen_id = id;
+ seen_params = params;
+ return LLSD();
+ });
+
+ LLSD params;
+ params["message"] = "hello";
+ LLSD notification = LLJSONRPCConnection::makeEnvelope(LLSD(), "runtime.debug", params, LLSD(), LLSD());
+ conn.processMessage(notification);
+
+ ensure("handler should be called", called);
+ ensure("notification id should be undefined", seen_id.isUndefined());
+ ensure_equals("payload should be forwarded", seen_params["message"].asString(), "hello");
+ }
+
+ template<> template<>
+ void JSONRPCWSTestObjectType::test<5>()
+ {
+ set_test_name("sweepTimeouts expires overdue callbacks");
+
+ TestJSONRPCConnection conn;
+ conn.setAuthenticated(true);
+
+ bool callback_called = false;
+ conn.testInjectPendingRequest(
+ "req_1",
+ LLTimer::getTotalSeconds() - 1.0,
+ [&](const LLSD& result, const LLSD& error)
+ {
+ callback_called = true;
+ ensure("timed out result should be undefined", result.isUndefined());
+ ensure_equals(
+ "timeout code",
+ error["code"].asInteger(),
+ LLJSONRPCConnection::RPCError::REQUEST_TIMEOUT);
+ });
+
+ ensure_equals("pending request should be tracked", conn.testPendingRequestCount(), (size_t)1);
+ conn.testSweepTimeouts();
+ ensure("timeout callback should be called", callback_called);
+ ensure_equals("pending request should be removed", conn.testPendingRequestCount(), (size_t)0);
+ }
+
+ template<> template<>
+ void JSONRPCWSTestObjectType::test<6>()
+ {
+ set_test_name("a connection not yet authenticated dispatches nothing, and everything once it is");
+
+ TestJSONRPCConnection conn;
+ ensure("unauthenticated unless told otherwise", !conn.isAuthenticated());
+
+ int calls = 0;
+ conn.registerMethod("object.list",
+ [&](const std::string&, const LLSD&, const LLSD&) -> LLSD
+ {
+ ++calls;
+ return LLSD();
+ });
+
+ conn.setAuthenticated(false);
+ ensure("not authenticated", !conn.isAuthenticated());
+ const LLSD request = LLJSONRPCConnection::makeEnvelope(LLSD("req_1"), "object.list", LLSD::emptyMap(), LLSD(), LLSD());
+ conn.processMessage(request);
+ const LLSD notification = LLJSONRPCConnection::makeEnvelope(LLSD(), "object.list", LLSD::emptyMap(), LLSD(), LLSD());
+ conn.processMessage(notification);
+ ensure_equals("neither a request nor a notification reaches the handler", calls, 0);
+
+ conn.setAuthenticated(true);
+ conn.processMessage(request);
+ ensure_equals("dispatched once authenticated", calls, 1);
+ }
+
+ template<> template<>
+ void JSONRPCWSTestObjectType::test<7>()
+ {
+ set_test_name("a connection not yet authenticated still takes the answers to its own calls");
+
+ TestJSONRPCConnection conn;
+ conn.setAuthenticated(false);
+
+ LLSD answer;
+ conn.testInjectPendingRequest(
+ "rpc_1",
+ LLTimer::getTotalSeconds() + 60.0,
+ [&](const LLSD& result, const LLSD& error)
+ {
+ ensure("no error", error.isUndefined());
+ answer = result;
+ });
+
+ LLSD result;
+ result["challenge_response"] = "secret";
+ conn.processMessage(LLJSONRPCConnection::makeEnvelope(LLSD("rpc_1"), std::string(), LLSD(), result, LLSD()));
+ ensure_equals("the answer reached the caller", answer["challenge_response"].asString(), std::string("secret"));
+ ensure("and it is answered", conn.testPendingRequestCount() == 0);
+ }
+}
+
+#endif \ No newline at end of file
diff --git a/indra/llcorehttp/tests/test_refcounted.hpp b/indra/llcorehttp/tests/test_refcounted.hpp
index c0c8e78413..eb23a25545 100644
--- a/indra/llcorehttp/tests/test_refcounted.hpp
+++ b/indra/llcorehttp/tests/test_refcounted.hpp
@@ -28,8 +28,6 @@
#include "_refcounted.h"
-// disable all of this because it's hanging win64 builds?
-#if ! (LL_WINDOWS && ADDRESS_SIZE == 64)
using namespace LLCoreInt;
namespace tut
@@ -122,5 +120,4 @@ namespace tut
ensure(rc->getRefCount() == RefCounted::NOT_REF_COUNTED);
}
}
-#endif // disabling on Win64
#endif // TEST_LLCOREINT_REF_COUNTED_H_
diff --git a/indra/llcorehttp/tests/test_websocketmgr.hpp b/indra/llcorehttp/tests/test_websocketmgr.hpp
new file mode 100644
index 0000000000..38b3d79cbf
--- /dev/null
+++ b/indra/llcorehttp/tests/test_websocketmgr.hpp
@@ -0,0 +1,157 @@
+/**
+ * @file test_websocketmgr.hpp
+ * @brief A WebSocket server lets programs in and turns pages away.
+ *
+ * $LicenseInfo:firstyear=2026&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2026, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ */
+
+#ifndef TEST_LLCORE_WEBSOCKETMGR_H_
+#define TEST_LLCORE_WEBSOCKETMGR_H_
+
+#include "llwebsocketmgr.h"
+
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <boost/asio/read_until.hpp>
+#include <boost/asio/streambuf.hpp>
+#include <boost/asio/write.hpp>
+
+#include <chrono>
+#include <istream>
+#include <random>
+
+namespace
+{
+ // A port nothing on this machine is listening on, as far as can be
+ // told; zero where none turned up.
+ U16 unusedPort()
+ {
+ std::random_device random;
+ for (int tries = 0; tries < 20; ++tries)
+ {
+ const U16 port = static_cast<U16>(40000 + random() % 20000);
+ boost::asio::io_context io;
+ boost::asio::ip::tcp::acceptor acceptor(io);
+ boost::system::error_code ec;
+ acceptor.open(boost::asio::ip::tcp::v4(), ec);
+ if (!ec)
+ {
+ acceptor.bind(boost::asio::ip::tcp::endpoint(boost::asio::ip::make_address("127.0.0.1"), port), ec);
+ }
+ if (!ec)
+ {
+ return port;
+ }
+ }
+ return 0;
+ }
+
+ // The HTTP status a WebSocket upgrade request from this origin -- none,
+ // where empty -- is answered with: 101 where it would open, -1 where
+ // nothing answered at all. The request is what a browser or a program
+ // sends, written out, so that what is tested is what the server says.
+ int upgradeFrom(U16 port, const std::string& origin)
+ {
+ boost::asio::io_context io;
+ boost::asio::ip::tcp::socket socket(io);
+ boost::system::error_code ec;
+ socket.connect(boost::asio::ip::tcp::endpoint(boost::asio::ip::make_address("127.0.0.1"), port), ec);
+ if (ec)
+ {
+ return -1;
+ }
+
+ std::string request = "GET / HTTP/1.1\r\n"
+ "Host: 127.0.0.1:" + std::to_string(port) + "\r\n"
+ "Upgrade: websocket\r\n"
+ "Connection: Upgrade\r\n"
+ "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
+ "Sec-WebSocket-Version: 13\r\n";
+ if (!origin.empty())
+ {
+ request += "Origin: " + origin + "\r\n";
+ }
+ request += "\r\n";
+ boost::asio::write(socket, boost::asio::buffer(request), ec);
+ if (ec)
+ {
+ return -1;
+ }
+
+ // The status line, or nothing within the time.
+ boost::asio::streambuf response;
+ bool answered = false;
+ boost::asio::async_read_until(socket, response, "\r\n",
+ [&](const boost::system::error_code& error, std::size_t) { answered = !error; });
+ io.run_for(std::chrono::seconds(10));
+ socket.close(ec);
+ if (!answered)
+ {
+ return -1;
+ }
+ std::istream line(&response);
+ std::string version;
+ int status = -1;
+ line >> version >> status;
+ return status;
+ }
+}
+
+namespace tut
+{
+ struct WebsocketMgrTestData
+ {
+ };
+
+ typedef test_group<WebsocketMgrTestData> WebsocketMgrTestGroupType;
+ typedef WebsocketMgrTestGroupType::object WebsocketMgrTestObjectType;
+ WebsocketMgrTestGroupType WebsocketMgrTestGroup("LLWebsocketMgr Tests");
+
+ template<> template<>
+ void WebsocketMgrTestObjectType::test<1>()
+ {
+ set_test_name("a program, which sends no origin, is let in; a page, which always does, is refused");
+
+ const U16 port = unusedPort();
+ if (!port)
+ {
+ skip("no port free to listen on");
+ }
+ const std::string name = "origin_test";
+ LLWebsocketMgr& manager = LLWebsocketMgr::instance();
+ ensure("added", manager.addServer(std::make_shared<LLWebsocketMgr::WSServer>(name, port, true)));
+ ensure("started", manager.startServer(name));
+
+ const int program = upgradeFrom(port, std::string());
+ const int page = upgradeFrom(port, "https://example.com");
+ const int opaque = upgradeFrom(port, "null");
+ const int local = upgradeFrom(port, "http://127.0.0.1:8080");
+ manager.removeServer(name);
+
+ ensure_equals("no origin: in", program, 101);
+ ensure_equals("a web page: refused", page, 403);
+ ensure_equals("a page with an opaque origin, a sandboxed frame or a file: refused", opaque, 403);
+ ensure_equals("a page served from this machine: refused", local, 403);
+ }
+}
+
+#endif
diff --git a/indra/llcorehttp/tests/testwsclient.h b/indra/llcorehttp/tests/testwsclient.h
new file mode 100644
index 0000000000..587934f2b6
--- /dev/null
+++ b/indra/llcorehttp/tests/testwsclient.h
@@ -0,0 +1,170 @@
+/**
+ * @file testwsclient.h
+ * @brief Minimal WebSocket client for driving a server under test
+ *
+ * $LicenseInfo:firstyear=2026&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2026, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ */
+
+#ifndef LL_TESTWSCLIENT_H
+#define LL_TESTWSCLIENT_H
+
+// MSVC does not report __cplusplus >= 201103L without /Zc:__cplusplus, so
+// websocketpp's own C++11 detection would otherwise fall back to
+// boost::random_device, which nothing here links.
+#define _WEBSOCKETPP_CPP11_RANDOM_DEVICE_
+
+#include <websocketpp/config/asio_no_tls_client.hpp>
+#include <websocketpp/client.hpp>
+
+#include <chrono>
+#include <condition_variable>
+#include <mutex>
+#include <string>
+#include <thread>
+#include <vector>
+
+// A minimal WebSocket client for driving a server under test: connects,
+// optionally with an Origin header, and can send and collect messages.
+class TestWSClient
+{
+public:
+ using client_t = websocketpp::client<websocketpp::config::asio_client>;
+
+ TestWSClient()
+ {
+ mClient.clear_access_channels(websocketpp::log::alevel::all);
+ mClient.clear_error_channels(websocketpp::log::elevel::all);
+ mClient.init_asio();
+
+ mClient.set_open_handler([this](websocketpp::connection_hdl hdl)
+ {
+ std::lock_guard<std::mutex> lock(mMutex);
+ mHandle = hdl;
+ mOpened = true;
+ mCondVar.notify_all();
+ });
+ mClient.set_fail_handler([this](websocketpp::connection_hdl)
+ {
+ std::lock_guard<std::mutex> lock(mMutex);
+ mFailed = true;
+ mCondVar.notify_all();
+ });
+ mClient.set_close_handler([this](websocketpp::connection_hdl)
+ {
+ std::lock_guard<std::mutex> lock(mMutex);
+ mClosed = true;
+ mCondVar.notify_all();
+ });
+ mClient.set_message_handler([this](websocketpp::connection_hdl, client_t::message_ptr msg)
+ {
+ std::lock_guard<std::mutex> lock(mMutex);
+ mReceived.push_back(msg->get_payload());
+ mCondVar.notify_all();
+ });
+ }
+
+ ~TestWSClient()
+ {
+ close();
+ mClient.stop();
+ if (mThread.joinable())
+ {
+ mThread.join();
+ }
+ }
+
+ // Initiates a close and waits for the ack, so teardown looks like a
+ // normal closure instead of a dropped socket. Safe to call more than
+ // once, and safe if the connection never opened.
+ void close(std::chrono::seconds timeout = std::chrono::seconds(2))
+ {
+ std::unique_lock<std::mutex> lock(mMutex);
+ if (!mOpened || mClosed)
+ {
+ return;
+ }
+ websocketpp::lib::error_code ec;
+ mClient.close(mHandle, websocketpp::close::status::normal, "test done", ec);
+ mCondVar.wait_for(lock, timeout, [this]() { return mClosed; });
+ }
+
+ // Connects to 127.0.0.1:port, optionally with an Origin header, and
+ // waits up to timeout for the handshake to open or fail.
+ // force_origin_header sends "Origin:" with an empty value instead of
+ // omitting the header entirely, for origin.empty().
+ bool connect(U16 port, const std::string& origin, bool force_origin_header = false,
+ std::chrono::seconds timeout = std::chrono::seconds(5))
+ {
+ websocketpp::lib::error_code ec;
+ client_t::connection_ptr con = mClient.get_connection(
+ "ws://127.0.0.1:" + std::to_string(port) + "/", ec);
+ if (ec)
+ {
+ return false;
+ }
+ if (!origin.empty() || force_origin_header)
+ {
+ con->append_header("Origin", origin);
+ }
+ mClient.connect(con);
+ mThread = std::thread([this]() { mClient.run(); });
+
+ std::unique_lock<std::mutex> lock(mMutex);
+ return mCondVar.wait_for(lock, timeout, [this]() { return mOpened || mFailed; }) && mOpened;
+ }
+
+ bool send(const std::string& message)
+ {
+ websocketpp::lib::error_code ec;
+ mClient.send(mHandle, message, websocketpp::frame::opcode::text, ec);
+ return !ec;
+ }
+
+ // Waits up to timeout for at least one message to arrive, then
+ // removes and returns it; empty where none arrived in time.
+ std::string waitForMessage(std::chrono::seconds timeout = std::chrono::seconds(5))
+ {
+ std::unique_lock<std::mutex> lock(mMutex);
+ if (!mCondVar.wait_for(lock, timeout, [this]() { return !mReceived.empty(); }))
+ {
+ return std::string();
+ }
+ std::string message = mReceived.front();
+ mReceived.erase(mReceived.begin());
+ return message;
+ }
+
+ bool isOpen() const { return mOpened && !mClosed; }
+
+private:
+ client_t mClient;
+ websocketpp::connection_hdl mHandle;
+ std::thread mThread;
+ std::mutex mMutex;
+ std::condition_variable mCondVar;
+ bool mOpened{ false };
+ bool mFailed{ false };
+ bool mClosed{ false };
+ std::vector<std::string> mReceived;
+};
+
+#endif // LL_TESTWSCLIENT_H
diff --git a/indra/llcorehttp/tests/wsserver_test.cpp b/indra/llcorehttp/tests/wsserver_test.cpp
new file mode 100644
index 0000000000..62fa66a9cd
--- /dev/null
+++ b/indra/llcorehttp/tests/wsserver_test.cpp
@@ -0,0 +1,438 @@
+/**
+ * @file wsserver_test.cpp
+ * @brief Security-focused unit tests for LLWebsocketMgr::WSServer
+ *
+ * $LicenseInfo:firstyear=2026&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2026, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ */
+
+#include "linden_common.h"
+
+#include "llwebsocketmgr.h"
+
+#include "testwsclient.h"
+#include "../test/lltut.h"
+
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/ip/tcp.hpp>
+#include <boost/asio/ip/udp.hpp>
+#include <boost/asio/read_until.hpp>
+#include <boost/asio/streambuf.hpp>
+#include <boost/asio/write.hpp>
+
+#include <atomic>
+#include <chrono>
+#include <istream>
+#include <random>
+#include <thread>
+
+namespace
+{
+ // A port nothing on this machine is listening on, as far as can be
+ // told; zero where none turned up.
+ U16 unusedPort()
+ {
+ std::random_device random;
+ for (int tries = 0; tries < 20; ++tries)
+ {
+ const U16 port = static_cast<U16>(40000 + random() % 20000);
+ boost::asio::io_context io;
+ boost::asio::ip::tcp::acceptor acceptor(io);
+ boost::system::error_code ec;
+ acceptor.open(boost::asio::ip::tcp::v4(), ec);
+ if (!ec)
+ {
+ acceptor.bind(boost::asio::ip::tcp::endpoint(boost::asio::ip::make_address("127.0.0.1"), port), ec);
+ }
+ if (!ec)
+ {
+ return port;
+ }
+ }
+ return 0;
+ }
+
+ // The HTTP status a WebSocket upgrade request from this origin -- none,
+ // where empty -- is answered with: 101 where it would open, -1 where
+ // nothing answered at all.
+ // force_origin_header sends "Origin:" with an empty value instead of
+ // omitting the header entirely, for origin.empty().
+ int upgradeFrom(U16 port, const std::string& origin, bool force_origin_header = false)
+ {
+ boost::asio::io_context io;
+ boost::asio::ip::tcp::socket socket(io);
+ boost::system::error_code ec;
+ socket.connect(boost::asio::ip::tcp::endpoint(boost::asio::ip::make_address("127.0.0.1"), port), ec);
+ if (ec)
+ {
+ return -1;
+ }
+
+ std::string request = "GET / HTTP/1.1\r\n"
+ "Host: 127.0.0.1:" + std::to_string(port) + "\r\n"
+ "Upgrade: websocket\r\n"
+ "Connection: Upgrade\r\n"
+ "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
+ "Sec-WebSocket-Version: 13\r\n";
+ if (!origin.empty() || force_origin_header)
+ {
+ request += "Origin: " + origin + "\r\n";
+ }
+ request += "\r\n";
+ boost::asio::write(socket, boost::asio::buffer(request), ec);
+ if (ec)
+ {
+ return -1;
+ }
+
+ boost::asio::streambuf response;
+ bool answered = false;
+ boost::asio::async_read_until(socket, response, "\r\n",
+ [&](const boost::system::error_code& error, std::size_t) { answered = !error; });
+ io.run_for(std::chrono::seconds(10));
+ socket.close(ec);
+ if (!answered)
+ {
+ return -1;
+ }
+ std::istream line(&response);
+ std::string version;
+ int status = -1;
+ line >> version >> status;
+ return status;
+ }
+
+ // Polls a predicate until it is true or the deadline passes. A client
+ // seeing its own handshake complete says nothing about whether the
+ // server's own thread has finished its side of onOpen bookkeeping yet.
+ template <typename Predicate>
+ bool waitUntil(Predicate predicate, std::chrono::milliseconds timeout = std::chrono::milliseconds(2000))
+ {
+ const auto deadline = std::chrono::steady_clock::now() + timeout;
+ while (!predicate())
+ {
+ if (std::chrono::steady_clock::now() >= deadline)
+ {
+ return false;
+ }
+ std::this_thread::sleep_for(std::chrono::milliseconds(10));
+ }
+ return true;
+ }
+
+ // The machine's own non-loopback IPv4 address, as seen when routing
+ // toward the internet (no packet is actually sent); empty where none
+ // could be found, such as a loopback-only sandbox.
+ std::string localNonLoopbackAddress()
+ {
+ boost::asio::io_context io;
+ boost::asio::ip::udp::socket socket(io);
+ boost::system::error_code ec;
+ socket.connect(boost::asio::ip::udp::endpoint(boost::asio::ip::make_address("8.8.8.8"), 53), ec);
+ if (ec)
+ {
+ return std::string();
+ }
+ return socket.local_endpoint(ec).address().to_string();
+ }
+
+ // Whether a TCP connection to host:port succeeds within a short time;
+ // refused or silently dropped are both treated as "no".
+ bool canConnect(const std::string& host, U16 port)
+ {
+ boost::asio::io_context io;
+ boost::asio::ip::tcp::socket socket(io);
+ boost::system::error_code result = boost::asio::error::would_block;
+ socket.async_connect(
+ boost::asio::ip::tcp::endpoint(boost::asio::ip::make_address(host), port),
+ [&](const boost::system::error_code& ec) { result = ec; });
+ io.run_for(std::chrono::seconds(3));
+ boost::system::error_code close_ec;
+ socket.close(close_ec);
+ return !result;
+ }
+
+ // Records what a server actually did, so tests can assert on lifecycle
+ // callbacks the base class leaves as no-ops.
+ class TestWSServer : public LLWebsocketMgr::WSServer
+ {
+ public:
+ using LLWebsocketMgr::WSServer::WSServer;
+
+ std::atomic<int> mOpenedCount{ 0 };
+ std::atomic<int> mClosedCount{ 0 };
+
+ // Empty where the base policy (reject any Origin) should apply;
+ // otherwise the one origin this test server lets in.
+ std::string mAllowedOrigin;
+
+ void onConnectionOpened(const LLWebsocketMgr::WSConnection::ptr_t&) override
+ {
+ ++mOpenedCount;
+ }
+
+ void onConnectionClosed(const LLWebsocketMgr::WSConnection::ptr_t&) override
+ {
+ ++mClosedCount;
+ }
+
+ bool acceptOrigin(const std::string& origin) const override
+ {
+ if (!mAllowedOrigin.empty() && origin == mAllowedOrigin)
+ {
+ return true;
+ }
+ return WSServer::acceptOrigin(origin);
+ }
+ };
+}
+
+namespace tut
+{
+ struct wsserver_data
+ {
+ U16 mPort;
+ std::string mName;
+
+ wsserver_data() :
+ mPort(unusedPort()),
+ mName("wsserver_test")
+ {
+ }
+
+ ~wsserver_data()
+ {
+ LLWebsocketMgr::instance().removeServer(mName);
+ }
+ };
+
+ typedef test_group<wsserver_data> wsserver_test;
+ typedef wsserver_test::object wsserver_object;
+ wsserver_test wsserver_testcase("LLWebsocketMgr::WSServer");
+
+ template<> template<>
+ void wsserver_object::test<1>()
+ {
+ set_test_name("a program, which sends no Origin header, is let in");
+
+ if (!mPort)
+ {
+ skip("no port free to listen on");
+ }
+
+ LLWebsocketMgr& manager = LLWebsocketMgr::instance();
+ auto server = std::make_shared<LLWebsocketMgr::WSServer>(mName, mPort, true);
+ ensure("added", manager.addServer(server));
+ ensure("started", manager.startServer(mName));
+
+ ensure_equals("no origin: connection opens", upgradeFrom(mPort, std::string()), 101);
+ ensure("server clears the abruptly closed raw connection", waitUntil([&]() { return server->getConnectionCount() == 0; }));
+
+ TestWSClient client;
+ ensure("no origin: client connects over the network", client.connect(mPort, std::string()));
+ ensure("server sees the open connection", waitUntil([&]() { return server->getConnectionCount() == 1; }));
+
+ client.close();
+ manager.removeServer(mName);
+ }
+
+ template<> template<>
+ void wsserver_object::test<2>()
+ {
+ set_test_name("a page, which sends an Origin header, is refused");
+
+ if (!mPort)
+ {
+ skip("no port free to listen on");
+ }
+
+ LLWebsocketMgr& manager = LLWebsocketMgr::instance();
+ auto server = std::make_shared<LLWebsocketMgr::WSServer>(mName, mPort, true);
+ ensure("added", manager.addServer(server));
+ ensure("started", manager.startServer(mName));
+
+ ensure_equals("a web page: refused", upgradeFrom(mPort, "https://example.com"), 403);
+
+ TestWSClient client;
+ ensure("a web page: client never opens over the network", !client.connect(mPort, "https://example.com"));
+ ensure_equals("server never sees a connection", server->getConnectionCount(), (size_t)0);
+
+ manager.removeServer(mName);
+ }
+
+ template<> template<>
+ void wsserver_object::test<3>()
+ {
+ set_test_name("a page with an opaque origin (a sandboxed frame or a file) is refused");
+
+ if (!mPort)
+ {
+ skip("no port free to listen on");
+ }
+
+ LLWebsocketMgr& manager = LLWebsocketMgr::instance();
+ auto server = std::make_shared<LLWebsocketMgr::WSServer>(mName, mPort, true);
+ ensure("added", manager.addServer(server));
+ ensure("started", manager.startServer(mName));
+
+ ensure_equals("an opaque origin: refused", upgradeFrom(mPort, "null"), 403);
+
+ TestWSClient client;
+ ensure("an opaque origin: client never opens over the network", !client.connect(mPort, "null"));
+ ensure_equals("server never sees a connection", server->getConnectionCount(), (size_t)0);
+
+ manager.removeServer(mName);
+ }
+
+ template<> template<>
+ void wsserver_object::test<4>()
+ {
+ set_test_name("a page served from this machine is refused");
+
+ if (!mPort)
+ {
+ skip("no port free to listen on");
+ }
+
+ LLWebsocketMgr& manager = LLWebsocketMgr::instance();
+ auto server = std::make_shared<LLWebsocketMgr::WSServer>(mName, mPort, true);
+ ensure("added", manager.addServer(server));
+ ensure("started", manager.startServer(mName));
+
+ ensure_equals("a page on this machine: refused", upgradeFrom(mPort, "http://127.0.0.1:8080"), 403);
+
+ TestWSClient client;
+ ensure("a page on this machine: client never opens over the network", !client.connect(mPort, "http://127.0.0.1:8080"));
+ ensure_equals("server never sees a connection", server->getConnectionCount(), (size_t)0);
+
+ manager.removeServer(mName);
+ }
+
+ template<> template<>
+ void wsserver_object::test<5>()
+ {
+ set_test_name("an Origin header present but empty is treated the same as none");
+
+ if (!mPort)
+ {
+ skip("no port free to listen on");
+ }
+
+ LLWebsocketMgr& manager = LLWebsocketMgr::instance();
+ auto server = std::make_shared<LLWebsocketMgr::WSServer>(mName, mPort, true);
+ ensure("added", manager.addServer(server));
+ ensure("started", manager.startServer(mName));
+
+ ensure_equals("empty origin: connection opens", upgradeFrom(mPort, std::string(), true), 101);
+ ensure("server clears the abruptly closed raw connection", waitUntil([&]() { return server->getConnectionCount() == 0; }));
+
+ TestWSClient client;
+ ensure("empty origin: client connects over the network", client.connect(mPort, std::string(), true));
+ ensure("server sees the open connection", waitUntil([&]() { return server->getConnectionCount() == 1; }));
+
+ client.close();
+ manager.removeServer(mName);
+ }
+
+ template<> template<>
+ void wsserver_object::test<7>()
+ {
+ set_test_name("local_only binds to loopback only; the machine's own LAN address is unreachable");
+
+ if (!mPort)
+ {
+ skip("no port free to listen on");
+ }
+
+ const std::string lan_address = localNonLoopbackAddress();
+ if (lan_address.empty())
+ {
+ skip("no non-loopback network interface to test against");
+ }
+
+ LLWebsocketMgr& manager = LLWebsocketMgr::instance();
+ auto server = std::make_shared<LLWebsocketMgr::WSServer>(mName, mPort, true);
+ ensure("added", manager.addServer(server));
+ ensure("started", manager.startServer(mName));
+
+ ensure("loopback: reachable", canConnect("127.0.0.1", mPort));
+ ensure("LAN address: unreachable", !canConnect(lan_address, mPort));
+
+ manager.removeServer(mName);
+ }
+
+ template<> template<>
+ void wsserver_object::test<8>()
+ {
+ set_test_name("a refused connection never reaches onConnectionOpened, and an accepted one does");
+
+ if (!mPort)
+ {
+ skip("no port free to listen on");
+ }
+
+ LLWebsocketMgr& manager = LLWebsocketMgr::instance();
+ auto server = std::make_shared<TestWSServer>(mName, mPort, true);
+ ensure("added", manager.addServer(server));
+ ensure("started", manager.startServer(mName));
+
+ ensure_equals("a web page: refused", upgradeFrom(mPort, "https://example.com"), 403);
+ ensure_equals("refused: onConnectionOpened never fires", server->mOpenedCount.load(), 0);
+
+ TestWSClient client;
+ ensure("no origin: client connects over the network", client.connect(mPort, std::string()));
+ ensure("accepted: onConnectionOpened fires once", waitUntil([&]() { return server->mOpenedCount.load() == 1; }));
+
+ client.close();
+ manager.removeServer(mName);
+ }
+
+ template<> template<>
+ void wsserver_object::test<9>()
+ {
+ set_test_name("multiple simultaneous connections are each tracked independently");
+
+ if (!mPort)
+ {
+ skip("no port free to listen on");
+ }
+
+ LLWebsocketMgr& manager = LLWebsocketMgr::instance();
+ auto server = std::make_shared<TestWSServer>(mName, mPort, true);
+ ensure("added", manager.addServer(server));
+ ensure("started", manager.startServer(mName));
+
+ TestWSClient client_a;
+ TestWSClient client_b;
+ TestWSClient client_c;
+ ensure("first client connects", client_a.connect(mPort, std::string()));
+ ensure("second client connects", client_b.connect(mPort, std::string()));
+ ensure("third client connects", client_c.connect(mPort, std::string()));
+
+ ensure("server counts all three connections", waitUntil([&]() { return server->getConnectionCount() == 3; }));
+ ensure("onConnectionOpened fires once per connection", waitUntil([&]() { return server->mOpenedCount.load() == 3; }));
+
+ client_a.close();
+ client_b.close();
+ client_c.close();
+ manager.removeServer(mName);
+ }
+}