/* * Copyright 2026 LiveKit * * 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. */ #include "livekit/token_source.h" #include #include #include #include #include #include "token_source_internal.h" namespace livekit { namespace { using TokenSourceResult = Result; using TokenSourceFuture = std::future; constexpr const char* kDefaultSandboxBaseUrl = "https://cloud-api.livekit.io"; bool tokenRequestOptionsEqual(const TokenRequestOptions& a, const TokenRequestOptions& b) { return a.room_name == b.room_name && a.participant_name == b.participant_name && a.participant_identity == b.participant_identity && a.participant_metadata == b.participant_metadata && a.participant_attributes == b.participant_attributes && a.agent_name == b.agent_name && a.agent_metadata == b.agent_metadata && a.agent_deployment == b.agent_deployment; } TokenSourceFuture makeFailedFuture(std::string message) { std::promise promise; promise.set_value(TokenSourceResult::failure(TokenSourceError{std::move(message)})); return promise.get_future(); } template TokenSourceFuture runAsyncTokenSource(std::string context, WorkFn&& work_fn) { try { return std::async(std::launch::async, [context = std::move(context), work_fn = std::forward(work_fn)]() mutable { try { return work_fn(); } catch (const std::exception& e) { return TokenSourceResult::failure(TokenSourceError{context + ": " + std::string(e.what())}); } catch (...) { return TokenSourceResult::failure(TokenSourceError{context + ": unknown exception"}); } }); } catch (const std::exception& e) { return makeFailedFuture(context + ": failed to start async work: " + std::string(e.what())); } catch (...) { return makeFailedFuture(context + ": failed to start async work: unknown exception"); } } std::string trimSandboxId(const std::string& sandbox_id) { const auto is_space = [](unsigned char ch) { return std::isspace(ch) != 0; }; const auto begin = std::find_if_not(sandbox_id.begin(), sandbox_id.end(), is_space); const auto end = std::find_if_not(sandbox_id.rbegin(), sandbox_id.rend(), is_space).base(); if (begin >= end) { return {}; } return std::string(begin, end); } std::string joinUrlPath(const std::string& base_url, const std::string& path) { if (base_url.empty()) { return path; } if (base_url.back() == '/') { if (path.empty()) { return base_url; } if (path.front() == '/') { return base_url + path.substr(1); } return base_url + path; } if (path.empty()) { return base_url; } if (path.front() == '/') { return base_url + path; } return base_url + "/" + path; } struct ResolvedSandboxEndpoint { std::string url; TokenEndpointOptions options; }; // Apply the sandbox header and resolve the connection-details URL shared by the // production and test-only sandbox factories. ResolvedSandboxEndpoint resolveSandboxEndpoint(const std::string& sandbox_id, TokenEndpointOptions options, const std::string& base_url) { options.headers["X-Sandbox-ID"] = trimSandboxId(sandbox_id); const std::string resolved_base_url = base_url.empty() ? kDefaultSandboxBaseUrl : base_url; return {joinUrlPath(resolved_base_url, "/api/v2/sandbox/connection-details"), std::move(options)}; } } // namespace std::unique_ptr LiteralTokenSource::create(std::string server_url, std::string participant_token) { TokenSourceResponse details; details.server_url = std::move(server_url); details.participant_token = std::move(participant_token); return std::unique_ptr(new LiteralTokenSource(std::move(details))); } std::unique_ptr LiteralTokenSource::create( std::function>()> provider) { return std::unique_ptr(new LiteralTokenSource(std::move(provider))); } LiteralTokenSource::LiteralTokenSource(TokenSourceResponse details) : details_(std::move(details)) {} LiteralTokenSource::LiteralTokenSource( std::function>()> provider) : provider_(std::move(provider)) {} std::future> LiteralTokenSource::fetch() { if (provider_) { return provider_(); } return std::async(std::launch::deferred, [details = details_]() { if (details.server_url.empty() || details.participant_token.empty()) { return Result::failure( TokenSourceError{"literal token source returned empty server_url or participant_token"}); } return Result::success(details); }); } std::unique_ptr CustomTokenSource::create( std::function>(const TokenRequestOptions&)> provider) { return std::unique_ptr(new CustomTokenSource(std::move(provider))); } CustomTokenSource::CustomTokenSource( std::function>(const TokenRequestOptions&)> provider) : provider_(std::move(provider)) {} std::future> CustomTokenSource::fetch( const TokenRequestOptions& options) { return provider_(options); } std::unique_ptr EndpointTokenSource::create(std::string endpoint_url, TokenEndpointOptions options) { return std::unique_ptr( new EndpointTokenSource(std::move(endpoint_url), std::move(options), &tokenSourceHttpRequest)); } EndpointTokenSource::EndpointTokenSource(std::string endpoint_url, TokenEndpointOptions options, HttpTransport transport) : endpoint_url_(std::move(endpoint_url)), options_(std::move(options)), transport_(std::move(transport)) {} std::unique_ptr EndpointTokenSourceTestAccess::create(std::string endpoint_url, TokenEndpointOptions options, TokenSourceHttpTransport transport) { return std::unique_ptr( new EndpointTokenSource(std::move(endpoint_url), std::move(options), std::move(transport))); } std::future> EndpointTokenSource::fetch( const TokenRequestOptions& options) { std::shared_ptr options_snapshot; try { options_snapshot = std::make_shared(options); } catch (const std::exception& e) { return makeFailedFuture("token source endpoint fetch failed: failed to copy request options: " + std::string(e.what())); } catch (...) { return makeFailedFuture("token source endpoint fetch failed: failed to copy request options: unknown exception"); } return runAsyncTokenSource("token source endpoint fetch failed", [this, options_snapshot]() { return fetchSync(*options_snapshot); }); } Result EndpointTokenSource::fetchSync(const TokenRequestOptions& options) const { const std::string request_json = buildTokenSourceRequestJson(options); auto headers = options_.headers; auto http_result = transport_(options_.method, endpoint_url_, headers, request_json, options_.timeout); if (!http_result) { return Result::failure( TokenSourceError{"token server request failed: " + http_result.error()}); } return parseTokenSourceResponseJson(http_result.value()); } std::unique_ptr SandboxTokenSource::create(const std::string& sandbox_id, const SandboxTokenServerOptions& options) { auto resolved = resolveSandboxEndpoint(sandbox_id, {}, options.base_url); auto endpoint = EndpointTokenSource::create(std::move(resolved.url), std::move(resolved.options)); return std::unique_ptr(new SandboxTokenSource(std::move(endpoint))); } SandboxTokenSource::SandboxTokenSource(std::unique_ptr endpoint) : endpoint_(std::move(endpoint)) {} std::unique_ptr SandboxTokenSourceTestAccess::create(const std::string& sandbox_id, const SandboxTokenServerOptions& options, TokenSourceHttpTransport transport) { auto resolved = resolveSandboxEndpoint(sandbox_id, {}, options.base_url); auto endpoint = EndpointTokenSourceTestAccess::create(std::move(resolved.url), std::move(resolved.options), std::move(transport)); return std::unique_ptr(new SandboxTokenSource(std::move(endpoint))); } std::future> SandboxTokenSource::fetch( const TokenRequestOptions& options) { return endpoint_->fetch(options); } std::unique_ptr CachingTokenSource::create(std::unique_ptr inner) { return std::unique_ptr(new CachingTokenSource(std::move(inner))); } CachingTokenSource::CachingTokenSource(std::unique_ptr inner) : inner_(std::move(inner)) {} std::future> CachingTokenSource::fetch( const TokenRequestOptions& options) { std::shared_ptr options_snapshot; try { options_snapshot = std::make_shared(options); } catch (const std::exception& e) { return makeFailedFuture("token source cache fetch failed: failed to copy request options: " + std::string(e.what())); } catch (...) { return makeFailedFuture("token source cache fetch failed: failed to copy request options: unknown exception"); } return runAsyncTokenSource("token source cache fetch failed", [this, options_snapshot]() { const std::scoped_lock lock(mutex_); if (cached_details_.has_value() && cached_options_.has_value() && tokenRequestOptionsEqual(*cached_options_, *options_snapshot) && isParticipantTokenValid(cached_details_->participant_token)) { return TokenSourceResult::success(*cached_details_); } auto result = inner_->fetch(*options_snapshot).get(); if (result) { cached_options_ = *options_snapshot; cached_details_ = result.value(); } return result; }); } void CachingTokenSource::invalidate() { const std::scoped_lock lock(mutex_); cached_options_.reset(); cached_details_.reset(); } std::optional CachingTokenSource::cachedResponse() const { const std::scoped_lock lock(mutex_); return cached_details_; } } // namespace livekit