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(); 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"; 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 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();