mirror of
https://github.com/morgan9e/FreeRDP
synced 2026-04-15 00:44:19 +09:00
Merge pull request #11518 from akallabeth/webview-update
Webview update
This commit is contained in:
@@ -19,33 +19,17 @@ set(WITH_WEBVIEW_DEFAULT OFF)
|
||||
|
||||
option(WITH_WEBVIEW "Build with WebView support for AAD login popup browser" ${WITH_WEBVIEW_DEFAULT})
|
||||
if(WITH_WEBVIEW)
|
||||
option(WITH_WEBVIEW_QT "Build with QtWebEngine support for AAD login broweser popup" OFF)
|
||||
|
||||
set(SRCS sdl_webview.hpp webview_impl.hpp sdl_webview.cpp)
|
||||
set(LIBS winpr)
|
||||
|
||||
if(WITH_WEBVIEW_QT)
|
||||
find_package(Qt5 COMPONENTS WebEngineWidgets REQUIRED)
|
||||
include(FetchContent)
|
||||
|
||||
list(APPEND SRCS qt/webview_impl.cpp)
|
||||
FetchContent_Declare(webview GIT_REPOSITORY https://github.com/akallabeth/webview GIT_TAG navigation-listener SYSTEM)
|
||||
FetchContent_MakeAvailable(webview)
|
||||
|
||||
list(APPEND LIBS Qt5::WebEngineWidgets)
|
||||
else()
|
||||
list(APPEND SRCS wrapper/webview.h wrapper/webview_impl.cpp)
|
||||
list(APPEND SRCS wrapper/webview_impl.cpp)
|
||||
|
||||
if(APPLE)
|
||||
find_library(WEBKIT Webkit REQUIRED)
|
||||
list(APPEND LIBS ${WEBKIT})
|
||||
elseif(NOT WIN32)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(WEBVIEW_GTK webkit2gtk-4.1)
|
||||
if(NOT WEBVIEW_GTK_FOUND)
|
||||
pkg_check_modules(WEBVIEW_GTK webkit2gtk-4.0 REQUIRED)
|
||||
endif()
|
||||
include_directories(SYSTEM ${WEBVIEW_GTK_INCLUDE_DIRS})
|
||||
list(APPEND LIBS ${WEBVIEW_GTK_LIBRARIES})
|
||||
endif()
|
||||
endif()
|
||||
list(APPEND LIBS webview::core)
|
||||
else()
|
||||
set(SRCS dummy.cpp)
|
||||
endif()
|
||||
@@ -59,7 +43,3 @@ set_property(TARGET sdl-common-aad-view PROPERTY FOLDER "Client/Common")
|
||||
target_include_directories(sdl-common-aad-view PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
|
||||
target_link_libraries(sdl-common-aad-view PRIVATE ${LIBS})
|
||||
target_compile_definitions(sdl-common-aad-view PUBLIC ${DEFINITIONS})
|
||||
if(WITH_WEBVIEW AND NOT WITH_WEBVIEW_QT)
|
||||
include(WebView2)
|
||||
target_link_webview2("sdl-common-aad-view")
|
||||
endif()
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
/**
|
||||
* FreeRDP: A Remote Desktop Protocol Implementation
|
||||
* Popup browser for AAD authentication
|
||||
*
|
||||
* Copyright 2023 Isaac Klein <fifthdegree@protonmail.com>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <QApplication>
|
||||
#include <QWebEngineView>
|
||||
#include <QWebEngineProfile>
|
||||
#include <QWebEngineUrlScheme>
|
||||
#include <QWebEngineUrlSchemeHandler>
|
||||
#include <QWebEngineUrlRequestJob>
|
||||
|
||||
#include <string>
|
||||
#include <cstdlib>
|
||||
#include <cstdarg>
|
||||
#include <winpr/string.h>
|
||||
#include <winpr/assert.h>
|
||||
#include <freerdp/log.h>
|
||||
#include <freerdp/build-config.h>
|
||||
|
||||
#include "../webview_impl.hpp"
|
||||
|
||||
#define TAG CLIENT_TAG("sdl.webview")
|
||||
|
||||
class SchemeHandler : public QWebEngineUrlSchemeHandler
|
||||
{
|
||||
public:
|
||||
explicit SchemeHandler(QObject* parent = nullptr) : QWebEngineUrlSchemeHandler(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void requestStarted(QWebEngineUrlRequestJob* request) override
|
||||
{
|
||||
QUrl url = request->requestUrl();
|
||||
|
||||
int rc = -1;
|
||||
for (auto& param : url.query().split('&'))
|
||||
{
|
||||
QStringList pair = param.split('=');
|
||||
|
||||
if (pair.size() != 2 || pair[0] != QLatin1String("code"))
|
||||
continue;
|
||||
|
||||
auto qc = pair[1];
|
||||
m_code = qc.toStdString();
|
||||
rc = 0;
|
||||
break;
|
||||
}
|
||||
qApp->exit(rc);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string code() const
|
||||
{
|
||||
return m_code;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string m_code{};
|
||||
};
|
||||
|
||||
bool webview_impl_run(const std::string& title, const std::string& url, std::string& code)
|
||||
{
|
||||
int argc = 1;
|
||||
const auto vendor = QLatin1String(FREERDP_VENDOR_STRING);
|
||||
const auto product = QLatin1String(FREERDP_PRODUCT_STRING);
|
||||
QWebEngineUrlScheme::registerScheme(QWebEngineUrlScheme("ms-appx-web"));
|
||||
|
||||
std::string wtitle = title;
|
||||
char* argv[] = { wtitle.data() };
|
||||
QCoreApplication::setOrganizationName(vendor);
|
||||
QCoreApplication::setApplicationName(product);
|
||||
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||
QApplication app(argc, argv);
|
||||
|
||||
SchemeHandler handler;
|
||||
QWebEngineProfile::defaultProfile()->installUrlSchemeHandler("ms-appx-web", &handler);
|
||||
|
||||
QWebEngineView webview;
|
||||
webview.load(QUrl(QString::fromStdString(url)));
|
||||
webview.show();
|
||||
|
||||
if (app.exec() != 0)
|
||||
return false;
|
||||
|
||||
auto val = handler.code();
|
||||
if (val.empty())
|
||||
return false;
|
||||
code = val;
|
||||
|
||||
return !code.empty();
|
||||
}
|
||||
@@ -61,14 +61,19 @@ static BOOL sdl_webview_get_rdsaad_access_token(freerdp* instance, const char* s
|
||||
WINPR_ASSERT(token);
|
||||
|
||||
WINPR_UNUSED(instance);
|
||||
WINPR_UNUSED(instance->context);
|
||||
|
||||
auto client_id = from_settings(instance->context->settings, FreeRDP_GatewayAvdClientID);
|
||||
auto context = instance->context;
|
||||
WINPR_UNUSED(context);
|
||||
|
||||
auto settings = context->settings;
|
||||
WINPR_ASSERT(settings);
|
||||
|
||||
auto client_id = from_settings(settings, FreeRDP_GatewayAvdClientID);
|
||||
std::string redirect_uri = "ms-appx-web%3a%2f%2fMicrosoft.AAD.BrokerPlugin%2f" + client_id;
|
||||
|
||||
*token = nullptr;
|
||||
|
||||
auto ep = from_aad_wellknown(instance->context, AAD_WELLKNOWN_authorization_endpoint);
|
||||
auto ep = from_aad_wellknown(context, AAD_WELLKNOWN_authorization_endpoint);
|
||||
auto url = ep + "?client_id=" + client_id + "&response_type=code&scope=" + scope +
|
||||
"&redirect_uri=" + redirect_uri;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "webview.h"
|
||||
#include <webview.h>
|
||||
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
@@ -25,45 +25,137 @@
|
||||
#include <map>
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
#include <cctype>
|
||||
#include <algorithm>
|
||||
#include "../webview_impl.hpp"
|
||||
|
||||
static std::vector<std::string> split(const std::string& input, const std::string& regex)
|
||||
{
|
||||
// passing -1 as the submatch index parameter performs splitting
|
||||
std::regex re(regex);
|
||||
std::sregex_token_iterator first{ input.begin(), input.end(), re, -1 };
|
||||
std::sregex_token_iterator last;
|
||||
return { first, last };
|
||||
}
|
||||
#include <winpr/string.h>
|
||||
#include <freerdp/log.h>
|
||||
|
||||
static std::map<std::string, std::string> urlsplit(const std::string& url)
|
||||
{
|
||||
auto pos = url.find('?');
|
||||
if (pos == std::string::npos)
|
||||
return {};
|
||||
auto surl = url.substr(pos);
|
||||
auto args = split(surl, "&");
|
||||
#define TAG FREERDP_TAG("client.SDL.common.aad")
|
||||
|
||||
std::map<std::string, std::string> argmap;
|
||||
for (const auto& arg : args)
|
||||
class fkt_arg
|
||||
{
|
||||
public:
|
||||
fkt_arg(const std::string& url)
|
||||
{
|
||||
auto kv = split(arg, "=");
|
||||
if (kv.size() == 2)
|
||||
argmap.insert({ kv[0], kv[1] });
|
||||
auto args = urlsplit(url);
|
||||
auto redir = args.find("redirect_uri");
|
||||
if (redir == args.end())
|
||||
{
|
||||
WLog_ERR(TAG, "[Webview] url %s does not contain a redirect_uri parameter, aborting.",
|
||||
url.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
_redirect_uri = from_url_encoded_str(redir->second);
|
||||
}
|
||||
}
|
||||
return argmap;
|
||||
}
|
||||
|
||||
static void fkt(const std::string& url, void* arg)
|
||||
bool valid() const
|
||||
{
|
||||
return !_redirect_uri.empty();
|
||||
}
|
||||
|
||||
bool getCode(std::string& c) const
|
||||
{
|
||||
c = _code;
|
||||
return !c.empty();
|
||||
}
|
||||
|
||||
bool handle(const std::string& uri) const
|
||||
{
|
||||
std::string duri = from_url_encoded_str(uri);
|
||||
if (duri.length() < _redirect_uri.length())
|
||||
return false;
|
||||
auto rc = _strnicmp(duri.c_str(), _redirect_uri.c_str(), _redirect_uri.length());
|
||||
return rc == 0;
|
||||
}
|
||||
|
||||
bool parse(const std::string& uri)
|
||||
{
|
||||
_args = urlsplit(uri);
|
||||
auto err = _args.find("error");
|
||||
if (err != _args.end())
|
||||
{
|
||||
auto suberr = _args.find("error_subcode");
|
||||
WLog_ERR(TAG, "[Webview] %s: %s, %s: %s", err->first.c_str(), err->second.c_str(),
|
||||
suberr->first.c_str(), suberr->second.c_str());
|
||||
return false;
|
||||
}
|
||||
auto val = _args.find("code");
|
||||
if (val == _args.end())
|
||||
{
|
||||
WLog_ERR(TAG, "[Webview] no code parameter detected in redirect URI %s", uri.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
_code = val->second;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
static std::string from_url_encoded_str(const std::string& str)
|
||||
{
|
||||
std::string cxxstr;
|
||||
auto cstr = winpr_str_url_decode(str.c_str(), str.length());
|
||||
if (cstr)
|
||||
{
|
||||
cxxstr = std::string(cstr);
|
||||
free(cstr);
|
||||
}
|
||||
return cxxstr;
|
||||
}
|
||||
|
||||
static std::vector<std::string> split(const std::string& input, const std::string& regex)
|
||||
{
|
||||
// passing -1 as the submatch index parameter performs splitting
|
||||
std::regex re(regex);
|
||||
std::sregex_token_iterator first{ input.begin(), input.end(), re, -1 };
|
||||
std::sregex_token_iterator last;
|
||||
return { first, last };
|
||||
}
|
||||
|
||||
static std::map<std::string, std::string> urlsplit(const std::string& url)
|
||||
{
|
||||
auto pos = url.find('?');
|
||||
if (pos == std::string::npos)
|
||||
return {};
|
||||
|
||||
pos++; // skip '?'
|
||||
auto surl = url.substr(pos);
|
||||
auto args = split(surl, "&");
|
||||
|
||||
std::map<std::string, std::string> argmap;
|
||||
for (const auto& arg : args)
|
||||
{
|
||||
auto kv = split(arg, "=");
|
||||
if (kv.size() == 2)
|
||||
argmap.insert({ kv[0], kv[1] });
|
||||
}
|
||||
|
||||
return argmap;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string _redirect_uri;
|
||||
std::string _code;
|
||||
std::map<std::string, std::string> _args;
|
||||
};
|
||||
|
||||
static void fkt(webview_t webview, const char* uri, webview_navigation_event_t type, void* arg)
|
||||
{
|
||||
auto args = urlsplit(url);
|
||||
auto val = args.find("code");
|
||||
if (val == args.end())
|
||||
assert(arg);
|
||||
auto rcode = static_cast<fkt_arg*>(arg);
|
||||
|
||||
if (type != WEBVIEW_LOAD_FINISHED)
|
||||
return;
|
||||
|
||||
assert(arg);
|
||||
auto rcode = static_cast<std::string*>(arg);
|
||||
*rcode = val->second;
|
||||
if (!rcode->handle(uri))
|
||||
return;
|
||||
|
||||
(void)rcode->parse(uri);
|
||||
webview_terminate(webview);
|
||||
}
|
||||
|
||||
bool webview_impl_run(const std::string& title, const std::string& url, std::string& code)
|
||||
@@ -71,12 +163,15 @@ bool webview_impl_run(const std::string& title, const std::string& url, std::str
|
||||
webview::webview w(false, nullptr);
|
||||
|
||||
w.set_title(title);
|
||||
w.set_size(640, 480, WEBVIEW_HINT_NONE);
|
||||
w.set_size(800, 600, WEBVIEW_HINT_NONE);
|
||||
|
||||
std::string scheme;
|
||||
w.add_scheme_handler("ms-appx-web", fkt, &scheme);
|
||||
w.add_navigate_listener(fkt, &code);
|
||||
fkt_arg arg(url);
|
||||
if (!arg.valid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
w.add_navigation_listener(fkt, &arg);
|
||||
w.navigate(url);
|
||||
w.run();
|
||||
return !code.empty();
|
||||
return arg.getCode(code);
|
||||
}
|
||||
|
||||
@@ -3,15 +3,18 @@ set(MODULE_PREFIX "TEST_SDL")
|
||||
|
||||
set(${MODULE_PREFIX}_DRIVER ${MODULE_NAME}.cpp)
|
||||
|
||||
set(${MODULE_PREFIX}_TESTS TestSDLPrefs.cpp)
|
||||
set(${MODULE_PREFIX}_TESTS TestSDLPrefs.cpp TestSDLWebview.cpp)
|
||||
|
||||
disable_warnings_for_directory(${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
create_test_sourcelist(${MODULE_PREFIX}_SRCS ${${MODULE_PREFIX}_DRIVER} ${${MODULE_PREFIX}_TESTS})
|
||||
|
||||
include_directories("${CMAKE_CURRENT_SOURCE_DIR}/../aad")
|
||||
include_directories("${CMAKE_CURRENT_BINARY_DIR}/../aad")
|
||||
|
||||
add_executable(${MODULE_NAME} ${${MODULE_PREFIX}_SRCS})
|
||||
|
||||
set(${MODULE_PREFIX}_LIBS freerdp winpr sdl-common-prefs)
|
||||
set(${MODULE_PREFIX}_LIBS freerdp freerdp-client winpr sdl-common-prefs sdl-common-aad-view)
|
||||
|
||||
target_link_libraries(${MODULE_NAME} ${${MODULE_PREFIX}_LIBS})
|
||||
|
||||
|
||||
30
client/SDL/common/test/TestSDLWebview.cpp
Normal file
30
client/SDL/common/test/TestSDLWebview.cpp
Normal file
@@ -0,0 +1,30 @@
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
|
||||
#include <winpr/config.h>
|
||||
#include <winpr/winpr.h>
|
||||
|
||||
#include <sdl_webview.hpp>
|
||||
|
||||
int TestSDLWebview(WINPR_ATTR_UNUSED int argc, WINPR_ATTR_UNUSED char* argv[])
|
||||
{
|
||||
#if 0
|
||||
RDP_CLIENT_ENTRY_POINTS entry = {};
|
||||
entry.Version = RDP_CLIENT_INTERFACE_VERSION;
|
||||
entry.Size = sizeof(RDP_CLIENT_ENTRY_POINTS_V1);
|
||||
entry.ContextSize = sizeof(rdpContext);
|
||||
|
||||
std::shared_ptr<rdpContext> context(freerdp_client_context_new(&entry),
|
||||
[](rdpContext* ptr) { freerdp_client_context_free(ptr); });
|
||||
|
||||
char* token = nullptr;
|
||||
if (!sdl_webview_get_access_token(context->instance, ACCESS_TOKEN_TYPE_AAD, &token, 2, "scope",
|
||||
"foobar"))
|
||||
{
|
||||
std::cerr << "test failed!" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
@@ -52,7 +52,7 @@ struct rdp_aad
|
||||
|
||||
#ifdef WITH_AAD
|
||||
|
||||
static BOOL aad_fetch_wellknown(rdpAad* aad);
|
||||
static BOOL aad_fetch_wellknown(wLog* log, rdpContext* context);
|
||||
static BOOL get_encoded_rsa_params(wLog* wlog, rdpPrivateKey* key, char** e, char** n);
|
||||
static BOOL generate_pop_key(rdpAad* aad);
|
||||
|
||||
@@ -180,6 +180,20 @@ static INLINE const char* aad_auth_result_to_string(DWORD code)
|
||||
return "Unknown error";
|
||||
}
|
||||
|
||||
static BOOL ensure_wellknown(rdpContext* context)
|
||||
{
|
||||
if (context->rdp->wellknown)
|
||||
return TRUE;
|
||||
|
||||
rdpAad* aad = context->rdp->aad;
|
||||
if (!aad)
|
||||
return FALSE;
|
||||
|
||||
if (!aad_fetch_wellknown(aad->log, context))
|
||||
return FALSE;
|
||||
return context->rdp->wellknown != NULL;
|
||||
}
|
||||
|
||||
static BOOL aad_get_nonce(rdpAad* aad)
|
||||
{
|
||||
BOOL ret = FALSE;
|
||||
@@ -194,6 +208,9 @@ static BOOL aad_get_nonce(rdpAad* aad)
|
||||
rdpRdp* rdp = aad->rdpcontext->rdp;
|
||||
WINPR_ASSERT(rdp);
|
||||
|
||||
if (!ensure_wellknown(aad->rdpcontext))
|
||||
return FALSE;
|
||||
|
||||
WINPR_JSON* obj = WINPR_JSON_GetObjectItem(rdp->wellknown, "token_endpoint");
|
||||
if (!obj)
|
||||
{
|
||||
@@ -292,7 +309,7 @@ int aad_client_begin(rdpAad* aad)
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!aad_fetch_wellknown(aad))
|
||||
if (!aad_fetch_wellknown(aad->log, aad->rdpcontext))
|
||||
return -1;
|
||||
|
||||
const BOOL arc = instance->GetAccessToken(instance, ACCESS_TOKEN_TYPE_AAD, &aad->access_token,
|
||||
@@ -756,12 +773,19 @@ int aad_client_begin(rdpAad* aad)
|
||||
WLog_Print(aad->log, WLOG_ERROR, "AAD security not compiled in, aborting!");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int aad_recv(rdpAad* aad, wStream* s)
|
||||
{
|
||||
WINPR_ASSERT(aad);
|
||||
WLog_Print(aad->log, WLOG_ERROR, "AAD security not compiled in, aborting!");
|
||||
return -1;
|
||||
}
|
||||
|
||||
static BOOL ensure_wellknown(WINPR_ATTR_UNUSED rdpContext* context)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
rdpAad* aad_new(rdpContext* context, rdpTransport* transport)
|
||||
@@ -855,26 +879,24 @@ cleanup:
|
||||
return token;
|
||||
}
|
||||
|
||||
BOOL aad_fetch_wellknown(rdpAad* aad)
|
||||
BOOL aad_fetch_wellknown(wLog* log, rdpContext* context)
|
||||
{
|
||||
WINPR_ASSERT(aad);
|
||||
WINPR_ASSERT(aad->rdpcontext);
|
||||
WINPR_ASSERT(context);
|
||||
|
||||
rdpRdp* rdp = aad->rdpcontext->rdp;
|
||||
rdpRdp* rdp = context->rdp;
|
||||
WINPR_ASSERT(rdp);
|
||||
|
||||
if (rdp->wellknown)
|
||||
return TRUE;
|
||||
|
||||
const char* base =
|
||||
freerdp_settings_get_string(aad->rdpcontext->settings, FreeRDP_GatewayAzureActiveDirectory);
|
||||
freerdp_settings_get_string(context->settings, FreeRDP_GatewayAzureActiveDirectory);
|
||||
const BOOL useTenant =
|
||||
freerdp_settings_get_bool(aad->rdpcontext->settings, FreeRDP_GatewayAvdUseTenantid);
|
||||
freerdp_settings_get_bool(context->settings, FreeRDP_GatewayAvdUseTenantid);
|
||||
const char* tenantid = "common";
|
||||
if (useTenant)
|
||||
tenantid =
|
||||
freerdp_settings_get_string(aad->rdpcontext->settings, FreeRDP_GatewayAvdAadtenantid);
|
||||
rdp->wellknown = freerdp_utils_aad_get_wellknown(aad->log, base, tenantid);
|
||||
tenantid = freerdp_settings_get_string(context->settings, FreeRDP_GatewayAvdAadtenantid);
|
||||
rdp->wellknown = freerdp_utils_aad_get_wellknown(log, base, tenantid);
|
||||
return rdp->wellknown ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
@@ -889,7 +911,7 @@ const char* freerdp_utils_aad_get_wellknown_custom_string(rdpContext* context, c
|
||||
WINPR_ASSERT(context);
|
||||
WINPR_ASSERT(context->rdp);
|
||||
|
||||
if (!context->rdp->wellknown)
|
||||
if (!ensure_wellknown(context))
|
||||
return NULL;
|
||||
|
||||
WINPR_JSON* obj = WINPR_JSON_GetObjectItem(context->rdp->wellknown, which);
|
||||
@@ -965,7 +987,7 @@ WINPR_JSON* freerdp_utils_aad_get_wellknown_custom_object(rdpContext* context, c
|
||||
WINPR_ASSERT(context);
|
||||
WINPR_ASSERT(context->rdp);
|
||||
|
||||
if (!context->rdp->wellknown)
|
||||
if (!ensure_wellknown(context))
|
||||
return NULL;
|
||||
|
||||
return WINPR_JSON_GetObjectItem(context->rdp->wellknown, which);
|
||||
|
||||
Reference in New Issue
Block a user