From 735e212c227844780ab11656ba6ec9c185192082 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 18:51:24 +0100 Subject: [PATCH 1/4] Add location spoofing UI and logic to settings Introduces a Location Spoofing section in the settings page, including UI for latitude/longitude input, preset buttons, and associated styles. Adds logic to hydrate, update, and save spoofed coordinates, and integrates with the settings' dirty state and auto-save functionality. Also updates the privacy settings list to include location spoofing. --- assets/settings.html | 228 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 227 insertions(+), 1 deletion(-) diff --git a/assets/settings.html b/assets/settings.html index a3cb8cc..42728b2 100644 --- a/assets/settings.html +++ b/assets/settings.html @@ -444,6 +444,96 @@ cursor: default; } + /* Location Spoofing styles */ + .location-row { + margin: 16px 0; + padding: 20px; + border-radius: 12px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + } + + .location-label { + font-size: 14px; + font-weight: 600; + margin-bottom: 12px; + color: var(--text-primary); + } + + .location-inputs { + display: flex; + gap: 16px; + margin-bottom: 12px; + } + + .location-field { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; + } + + .location-field label { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.03em; + color: var(--text-secondary); + } + + .location-input { + width: 100%; + padding: 10px 12px; + border-radius: 8px; + border: 1px solid var(--border-color); + background: var(--bg-tertiary); + color: var(--text-primary); + font-size: 13px; + font-family: 'Consolas', 'Courier New', monospace; + } + + .location-input:focus { + outline: none; + border-color: var(--accent-color); + } + + .location-input:disabled { + opacity: 0.5; + cursor: default; + } + + .location-help { + font-size: 12px; + color: var(--text-tertiary); + } + + .location-presets { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 12px; + } + + .location-preset { + padding: 6px 12px; + border-radius: 6px; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + color: var(--text-secondary); + font-size: 11px; + cursor: pointer; + transition: all 0.15s ease; + } + + .location-preset:hover { + background: var(--bg-hover); + color: var(--text-primary); + } + + .location-preset:disabled { + opacity: 0.5; + cursor: default; + } + /* Dark Theme Excluded Sites styles */ .dark-sites-row { margin: 32px 0 24px; @@ -560,6 +650,29 @@

DRM WebView

Enter URL patterns for sites where dark theme should be disabled (one per line). Supports wildcards (*) and comments starting with #. Browser internal pages (settings, history, etc.) are automatically excluded.
+ +
+
Location Spoofing
+
+
+ + +
+
+ + +
+
+
Enter GPS coordinates to spoof. Enable "Spoof location" in Privacy settings above to activate. Latitude: -90 to 90, Longitude: -180 to 180.
+
+ + + + + + +
+
Target User Agent String
@@ -611,7 +724,8 @@

DRM WebView

{ key: 'enable_javascript', name: 'Enable JavaScript', description: 'Allow JavaScript execution' }, { key: 'enable_web_security', name: 'Web Security', description: 'Enforce same-origin policy' }, { key: 'block_third_party_cookies', name: 'Block Third-Party Cookies', description: 'Prevent cross-site tracking' }, - { key: 'do_not_track', name: 'Do Not Track', description: 'Send DNT header' } + { key: 'do_not_track', name: 'Do Not Track', description: 'Send DNT header' }, + { key: 'enable_location_spoofing', name: 'Location Spoofing', description: 'Override GPS coordinates sent to websites' } ] }, drm: { @@ -667,6 +781,8 @@

DRM WebView

let currentSettings = {}, originalSettings = {}, isDirty = false; let targetUserAgent = '', originalTargetUserAgent = ''; let darkThemeExcludedSites = '', originalDarkThemeExcludedSites = ''; + let spoofedLatitude = 0, originalSpoofedLatitude = 0; + let spoofedLongitude = 0, originalSpoofedLongitude = 0; const drmElements = {}; let drmStatusSnapshot = null; @@ -732,6 +848,21 @@

DRM WebView

darkThemeExcludedSites = ''; originalDarkThemeExcludedSites = ''; } + // Hydrate location spoofing coordinates from payload + if (typeof data.spoofed_latitude === 'number') { + spoofedLatitude = data.spoofed_latitude; + originalSpoofedLatitude = data.spoofed_latitude; + } else { + spoofedLatitude = 0; + originalSpoofedLatitude = 0; + } + if (typeof data.spoofed_longitude === 'number') { + spoofedLongitude = data.spoofed_longitude; + originalSpoofedLongitude = data.spoofed_longitude; + } else { + spoofedLongitude = 0; + originalSpoofedLongitude = 0; + } if (data.meta && data.meta.storage_path) { document.getElementById('storage-path').textContent = 'Storage: ' + data.meta.storage_path; } @@ -746,6 +877,7 @@

DRM WebView

renderSettings(); hydrateUserAgentEditor(); hydrateDarkThemeExcludedSitesEditor(); + hydrateLocationEditor(); updateDirtyState(); loadDrmStatus(true); } @@ -842,6 +974,12 @@

DRM WebView

if (!isDirty && darkThemeExcludedSites !== originalDarkThemeExcludedSites) { isDirty = true; } + if (!isDirty && spoofedLatitude !== originalSpoofedLatitude) { + isDirty = true; + } + if (!isDirty && spoofedLongitude !== originalSpoofedLongitude) { + isDirty = true; + } document.getElementById('btn-save').disabled = !isDirty; } @@ -852,6 +990,8 @@

DRM WebView

originalSettings = { ...currentSettings }; originalTargetUserAgent = targetUserAgent; originalDarkThemeExcludedSites = darkThemeExcludedSites; + originalSpoofedLatitude = spoofedLatitude; + originalSpoofedLongitude = spoofedLongitude; isDirty = false; document.getElementById('btn-save').disabled = true; log('Saved'); @@ -881,9 +1021,25 @@

DRM WebView

darkThemeExcludedSites = ''; originalDarkThemeExcludedSites = ''; } + // Reset location spoofing coordinates to defaults + if (typeof data.spoofed_latitude === 'number') { + spoofedLatitude = data.spoofed_latitude; + originalSpoofedLatitude = data.spoofed_latitude; + } else { + spoofedLatitude = 0; + originalSpoofedLatitude = 0; + } + if (typeof data.spoofed_longitude === 'number') { + spoofedLongitude = data.spoofed_longitude; + originalSpoofedLongitude = data.spoofed_longitude; + } else { + spoofedLongitude = 0; + originalSpoofedLongitude = 0; + } renderSettings(); hydrateUserAgentEditor(); hydrateDarkThemeExcludedSitesEditor(); + hydrateLocationEditor(); updateDirtyState(); loadDrmStatus(true); log('Restored'); @@ -934,6 +1090,76 @@

DRM WebView

}; } + function hydrateLocationEditor() { + const latInput = document.getElementById('location-lat'); + const lngInput = document.getElementById('location-lng'); + const presetButtons = document.querySelectorAll('.location-preset'); + + if (latInput) { + latInput.value = spoofedLatitude || 0; + const locationEnabled = !!currentSettings.enable_location_spoofing; + latInput.disabled = !locationEnabled; + latInput.oninput = null; + latInput.oninput = () => { + let val = parseFloat(latInput.value) || 0; + val = Math.max(-90, Math.min(90, val)); + spoofedLatitude = val; + if (typeof window.OnUpdateSetting === 'function') { + window.OnUpdateSetting('spoofed_latitude', val); + } + updateDirtyState(); + const autoSave = !!currentSettings.auto_save_settings; + if (autoSave && typeof window.OnSaveSettings === 'function') { + window.OnSaveSettings(); + } + }; + } + + if (lngInput) { + lngInput.value = spoofedLongitude || 0; + const locationEnabled = !!currentSettings.enable_location_spoofing; + lngInput.disabled = !locationEnabled; + lngInput.oninput = null; + lngInput.oninput = () => { + let val = parseFloat(lngInput.value) || 0; + val = Math.max(-180, Math.min(180, val)); + spoofedLongitude = val; + if (typeof window.OnUpdateSetting === 'function') { + window.OnUpdateSetting('spoofed_longitude', val); + } + updateDirtyState(); + const autoSave = !!currentSettings.auto_save_settings; + if (autoSave && typeof window.OnSaveSettings === 'function') { + window.OnSaveSettings(); + } + }; + } + + // Handle preset buttons + presetButtons.forEach(btn => { + const locationEnabled = !!currentSettings.enable_location_spoofing; + btn.disabled = !locationEnabled; + btn.onclick = null; + btn.onclick = () => { + const lat = parseFloat(btn.dataset.lat) || 0; + const lng = parseFloat(btn.dataset.lng) || 0; + spoofedLatitude = lat; + spoofedLongitude = lng; + if (latInput) latInput.value = lat; + if (lngInput) lngInput.value = lng; + if (typeof window.OnUpdateSetting === 'function') { + window.OnUpdateSetting('spoofed_latitude', lat); + window.OnUpdateSetting('spoofed_longitude', lng); + } + updateDirtyState(); + const autoSave = !!currentSettings.auto_save_settings; + if (autoSave && typeof window.OnSaveSettings === 'function') { + window.OnSaveSettings(); + } + }; + }); + } + // Search helpers function applySettingsSearch(term) { window.__settingsSearchTerm = (term || '').toString(); From 823ae96e64ca8e3744ff5d71f724d1f1ac38795b Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 18:51:29 +0100 Subject: [PATCH 2/4] Add spoofed latitude and longitude settings Introduces support for spoofed_latitude and spoofed_longitude in settings. Adds lenient double parsing for these values when loading from disk and ensures they are saved to the settings file. --- src/Settings.cpp | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/Settings.cpp b/src/Settings.cpp index eeb9a82..f64d22f 100644 --- a/src/Settings.cpp +++ b/src/Settings.cpp @@ -78,6 +78,39 @@ namespace } return result; } + + double ParseDoubleLenient(const std::string &buffer, const std::string &key, double fallback) + { + if (key.empty()) + return fallback; + std::string needle = std::string("\"") + key + "\""; + auto pos = buffer.find(needle); + if (pos == std::string::npos) + return fallback; + pos = buffer.find(':', pos + needle.size()); + if (pos == std::string::npos) + return fallback; + ++pos; + while (pos < buffer.size() && std::isspace(static_cast(buffer[pos]))) + ++pos; + if (pos >= buffer.size()) + return fallback; + // Parse number (may be negative, may have decimal point) + std::string numStr; + while (pos < buffer.size() && (buffer[pos] == '-' || buffer[pos] == '+' || buffer[pos] == '.' || + std::isdigit(static_cast(buffer[pos])))) + { + numStr += buffer[pos]; + ++pos; + } + if (numStr.empty()) + return fallback; + try { + return std::stod(numStr); + } catch (...) { + return fallback; + } + } } void SettingsManager::EnsureDataDirectoryExists() @@ -147,6 +180,9 @@ bool SettingsManager::LoadSettingsFromDisk(UI &ui) // Parse string settings ui.settings_.custom_user_agent = ParseStringLenient(content, "custom_user_agent", ""); ui.settings_.dark_theme_excluded_sites = ParseStringLenient(content, "dark_theme_excluded_sites", ""); + // Parse location spoofing coordinates + ui.settings_.spoofed_latitude = ParseDoubleLenient(content, "spoofed_latitude", 0.0); + ui.settings_.spoofed_longitude = ParseDoubleLenient(content, "spoofed_longitude", 0.0); ui.settings_storage_path_ = (migrated ? legacy_path.string() : primary_path.string()); } else @@ -182,6 +218,8 @@ bool SettingsManager::SaveSettingsToDisk(UI &ui) doc << " \"values\": " << ui.BuildSettingsJSON() << ",\n"; doc << " \"custom_user_agent\": \"" << util::EscapeJsonString(ui.settings_.custom_user_agent) << "\",\n"; doc << " \"dark_theme_excluded_sites\": \"" << util::EscapeJsonString(ui.settings_.dark_theme_excluded_sites) << "\",\n"; + doc << " \"spoofed_latitude\": " << ui.settings_.spoofed_latitude << ",\n"; + doc << " \"spoofed_longitude\": " << ui.settings_.spoofed_longitude << ",\n"; doc << " \"meta\": {\n"; doc << " \"updated_at\": \"" << util::ToIso8601UTC(std::chrono::system_clock::now()) << "\",\n"; doc << " \"dirty\": false,\n"; From 54ae1e322a69a497e80a8c0de4599d170585bdb6 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 18:51:33 +0100 Subject: [PATCH 3/4] Update Tab.cpp --- src/Tab.cpp | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/Tab.cpp b/src/Tab.cpp index 5463dbb..62b1830 100644 --- a/src/Tab.cpp +++ b/src/Tab.cpp @@ -460,6 +460,70 @@ void Tab::OnWindowObjectReady(View *caller, uint64_t frame_id, bool is_main_fram })(); )JS"; caller->EvaluateScript(String(cryptoPolyfill), nullptr); + + // Inject location spoofing if enabled in settings + // This overrides navigator.geolocation to report custom coordinates + if (ui_->location_spoofing_enabled()) + { + double lat = ui_->spoofed_latitude(); + double lng = ui_->spoofed_longitude(); + std::ostringstream geoScript; + geoScript << R"JS( +(function() { + 'use strict'; + var spoofedLat = )JS" << lat << R"JS(; + var spoofedLng = )JS" << lng << R"JS(; + + // Create a fake GeolocationPosition object + function createPosition() { + return { + coords: { + latitude: spoofedLat, + longitude: spoofedLng, + accuracy: 10, + altitude: null, + altitudeAccuracy: null, + heading: null, + speed: null + }, + timestamp: Date.now() + }; + } + + // Override getCurrentPosition + var originalGetCurrentPosition = navigator.geolocation.getCurrentPosition; + navigator.geolocation.getCurrentPosition = function(success, error, options) { + console.log('[Ultralight] Geolocation spoofed to:', spoofedLat, spoofedLng); + setTimeout(function() { + success(createPosition()); + }, 100); + }; + + // Override watchPosition + var watchId = 0; + var watches = {}; + navigator.geolocation.watchPosition = function(success, error, options) { + var id = ++watchId; + console.log('[Ultralight] Geolocation watch spoofed to:', spoofedLat, spoofedLng); + watches[id] = setInterval(function() { + success(createPosition()); + }, 1000); + return id; + }; + + // Override clearWatch + navigator.geolocation.clearWatch = function(id) { + if (watches[id]) { + clearInterval(watches[id]); + delete watches[id]; + } + }; + + console.log('[Ultralight] Location spoofing enabled:', spoofedLat, spoofedLng); +})(); +)JS"; + caller->EvaluateScript(String(geoScript.str().c_str()), nullptr); + } // Inject Do Not Track (DNT) header simulation if enabled in settings // This overrides navigator.doNotTrack to report the user's preference From ff2072af67f65aea372a1beb38548d6085558d39 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 18:51:40 +0100 Subject: [PATCH 4/4] Add location spoofing settings and support Introduces location spoofing options to browser settings, allowing users to override navigator.geolocation with custom latitude and longitude values. Updates settings catalog, UI handlers, and payload builder to support enabling spoofing and specifying coordinates. --- src/UI.cpp | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- src/UI.h | 10 +++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/UI.cpp b/src/UI.cpp index 7f974ce..9c208b9 100644 --- a/src/UI.cpp +++ b/src/UI.cpp @@ -58,7 +58,7 @@ namespace bool default_value; }; - constexpr std::array kFallbackSettingsCatalog = { + constexpr std::array kFallbackSettingsCatalog = { // Appearance SettingDescriptor{"launch_dark_theme", "Launch in dark theme", "Start Ultralight with dark chrome, toolbars, and tabs by default.", @@ -169,7 +169,12 @@ namespace // Networking / User Agent SettingDescriptor{"use_custom_user_agent", "Use custom user agent", "When enabled, send a user agent string that you specify instead of the automatic Chromium-like default.", - "privacy", nullptr, false, &UI::BrowserSettings::use_custom_user_agent, false}}; + "privacy", nullptr, false, &UI::BrowserSettings::use_custom_user_agent, false}, + + // Location Spoofing + SettingDescriptor{"enable_location_spoofing", "Location Spoofing", + "Override navigator.geolocation to report custom GPS coordinates instead of your real location.", + "privacy", nullptr, false, &UI::BrowserSettings::enable_location_spoofing, false}}; struct ParsedCatalogEntry { @@ -3135,6 +3140,55 @@ void UI::OnUpdateSetting(const JSObject &, const JSArgs &args) UpdateSettingsDirtyFlag(); return; } + + // Special-case: spoofed_latitude is a numeric value for location spoofing + if (key == "spoofed_latitude") + { + double val = 0.0; + if (args[1].IsNumber()) + { + val = args[1].ToNumber(); + } + else if (args[1].IsString()) + { + ultralight::String str_ul = args[1].ToString(); + auto str_data = str_ul.utf8(); + std::string str = str_data.data() ? str_data.data() : ""; + try { val = std::stod(str); } catch (...) { val = 0.0; } + } + // Clamp to valid latitude range + val = (std::max)(-90.0, (std::min)(90.0, val)); + settings_.spoofed_latitude = val; + UpdateSettingsDirtyFlag(); + ApplySettings(false, false); + UpdateSettingsDirtyFlag(); + return; + } + + // Special-case: spoofed_longitude is a numeric value for location spoofing + if (key == "spoofed_longitude") + { + double val = 0.0; + if (args[1].IsNumber()) + { + val = args[1].ToNumber(); + } + else if (args[1].IsString()) + { + ultralight::String str_ul = args[1].ToString(); + auto str_data = str_ul.utf8(); + std::string str = str_data.data() ? str_data.data() : ""; + try { val = std::stod(str); } catch (...) { val = 0.0; } + } + // Clamp to valid longitude range + val = (std::max)(-180.0, (std::min)(180.0, val)); + settings_.spoofed_longitude = val; + UpdateSettingsDirtyFlag(); + ApplySettings(false, false); + UpdateSettingsDirtyFlag(); + return; + } + bool value = false; if (args[1].IsBoolean()) { @@ -3591,6 +3645,9 @@ std::string UI::BuildSettingsPayload(bool snapshot_is_baseline) const ss << "\"target_user_agent\": \"" << util::EscapeJsonString(settings_.custom_user_agent.empty() ? active_user_agent_ : settings_.custom_user_agent) << "\","; // Expose dark_theme_excluded_sites as a separate field for the text input in settings UI ss << "\"dark_theme_excluded_sites\": \"" << util::EscapeJsonString(settings_.dark_theme_excluded_sites) << "\","; + // Expose location spoofing coordinates + ss << "\"spoofed_latitude\": " << settings_.spoofed_latitude << ","; + ss << "\"spoofed_longitude\": " << settings_.spoofed_longitude << ","; ss << "\"meta\": {"; ss << "\"updated_at\": \"" << util::ToIso8601UTC(std::chrono::system_clock::now()) << "\","; ss << "\"dirty\": " << (settings_dirty_ ? "true" : "false") << ","; diff --git a/src/UI.h b/src/UI.h index 38e918d..4dd28fa 100644 --- a/src/UI.h +++ b/src/UI.h @@ -106,6 +106,11 @@ class UI : public WindowListener, bool restore_session_on_startup = true; // Restore previous session tabs on startup bool save_session_continuously = true; // Save session state continuously (crash recovery) + // Location Spoofing + bool enable_location_spoofing = false; // When true, override navigator.geolocation + double spoofed_latitude = 0.0; // Latitude to report (-90 to 90) + double spoofed_longitude = 0.0; // Longitude to report (-180 to 180) + bool operator==(const BrowserSettings &other) const; bool operator!=(const BrowserSettings &other) const { return !(*this == other); } }; @@ -223,6 +228,11 @@ class UI : public WindowListener, bool do_not_track_enabled() const { return settings_.do_not_track; } bool block_third_party_cookies_enabled() const { return settings_.block_third_party_cookies; } bool web_security_enabled() const { return settings_.enable_web_security; } + + // Location spoofing accessors for Tab's JavaScript injection + bool location_spoofing_enabled() const { return settings_.enable_location_spoofing; } + double spoofed_latitude() const { return settings_.spoofed_latitude; } + double spoofed_longitude() const { return settings_.spoofed_longitude; } protected: static std::filesystem::path SettingsDirectory();