Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions NativeScript/runtime/DevFlags.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ namespace tns {
// Controlled by package.json setting: "logScriptLoading": true|false
bool IsScriptLoadingLogEnabled();

// HTTP module loader flags
//
// Returns true when speculative HTTP module prefetching (the dep-graph BFS
// kicked off after each successful HttpFetchText) should be enabled. Default
// OFF so cold-boot behaviour is unchanged for users who have not opted in.
// Controlled by package.json / nativescript.config: "httpModulePrefetch": true|false
bool IsHttpModulePrefetchEnabled();

// Returns true when one log line should be emitted per HTTP fetch URL.
// Default OFF because the volume is high (one line per fetch, hundreds per
// cold boot, hundreds per HMR refresh). Opt in via package.json /
// nativescript.config: "httpFetchUrlLog": true|false
bool IsHttpFetchUrlLogEnabled();

// Security config

// In debug mode (RuntimeConfig.IsDebug): always returns true.
Expand Down
92 changes: 85 additions & 7 deletions NativeScript/runtime/DevFlags.mm
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#import <Foundation/Foundation.h>

#include "DevFlags.h"
#include "Helpers.h"
#include "Runtime.h"
#include "RuntimeConfig.h"
#include <vector>
Expand All @@ -13,16 +14,93 @@ bool IsScriptLoadingLogEnabled() {
return value ? [value boolValue] : false;
}

// HTTP module loader flags

// Reads `httpModulePrefetch` from app config (default: DISABLED).
//
// Apps that want to opt in for testing can set:
//
// // nativescript.config.ts
// export default {
// httpModulePrefetch: true,
// } as NativeScriptConfig;
//
// Returning false here short-circuits both the cache lookup and the prefetch
// wave in HttpFetchText, restoring the pre-prefetcher behavior bit-for-bit.
bool IsHttpModulePrefetchEnabled() {
static std::once_flag s_initFlag;
static bool s_enabled = false;
std::call_once(s_initFlag, []() {
@autoreleasepool {
id value = Runtime::GetAppConfigValue("httpModulePrefetch");
if (value && [value respondsToSelector:@selector(boolValue)]) {
s_enabled = [value boolValue];
}
}
// Startup banner. Gated on the logScriptLoading flag so it stays silent
// by default — flip the flag in nativescript.config.ts when diagnosing
// why prefetch is or isn't engaging.
//
// [http-loader] prefetch=disabled ← expected default
// [http-loader] prefetch=enabled ← only if config opt-in
if (IsScriptLoadingLogEnabled()) {
Log(@"[http-loader] prefetch=%s shared-session=on hmr-kickstart=on",
s_enabled ? "enabled" : "disabled");
}
});
return s_enabled;
}

// Default OFF because the volume is high (one line per fetch, hundreds per
// cold boot, hundreds per HMR refresh). Opt in via `nativescript.config.ts`:
//
// export default {
// httpFetchUrlLog: true, // turn on for diagnosis only
// …
// };
bool IsHttpFetchUrlLogEnabled() {
static std::once_flag s_initFlag;
static bool s_enabled = false;
std::call_once(s_initFlag, []() {
@autoreleasepool {
id value = Runtime::GetAppConfigValue("httpFetchUrlLog");
if (value && [value respondsToSelector:@selector(boolValue)]) {
s_enabled = [value boolValue];
}
}
if (IsScriptLoadingLogEnabled()) {
Log(@"[http-loader] fetch-url-log=%s",
s_enabled ? "enabled" : "disabled");
}
});
return s_enabled;
}

// Security config

static std::once_flag s_securityConfigInitFlag;
static bool s_allowRemoteModules = false;
static std::vector<std::string> s_remoteModuleAllowlist;

// Helper to check if a URL starts with a given prefix
static bool UrlStartsWith(const std::string& url, const std::string& prefix) {
if (prefix.size() > url.size()) return false;
return url.compare(0, prefix.size(), prefix) == 0;
// Returns true when `url` is authorized by allowlist `entry`.
//
// This is intentionally stricter than a raw string-prefix test: after the
// matched entry text, the next character in `url` must be a URL-component
// boundary ('/', '?', or '#'), the URL must end exactly at the entry, or the
// entry must itself end in '/'. That refuses lookalike-host and lookalike-port
// bypasses — an entry of "https://cdn.example.com" must NOT authorize
// "https://cdn.example.com.attacker.com/x.js" or
// "https://cdn.example.com:9999/x.js". To allow a specific port, include it in
// the allowlist entry (deny-by-default for anything not explicitly listed).
static bool RemoteUrlMatchesAllowlistEntry(const std::string& url,
const std::string& entry) {
if (entry.empty()) return false;
if (url.size() < entry.size()) return false;
if (url.compare(0, entry.size(), entry) != 0) return false;
if (url.size() == entry.size()) return true; // exact match
if (entry.back() == '/') return true; // entry ended at a boundary
const char next = url[entry.size()];
return next == '/' || next == '?' || next == '#';
}

void InitializeSecurityConfig() {
Expand Down Expand Up @@ -84,9 +162,9 @@ bool IsRemoteUrlAllowed(const std::string& url) {
return true;
}

// Check if URL matches any allowlist prefix
for (const std::string& prefix : s_remoteModuleAllowlist) {
if (UrlStartsWith(url, prefix)) {
// Check if URL matches any allowlist entry on a URL-component boundary.
for (const std::string& entry : s_remoteModuleAllowlist) {
if (RemoteUrlMatchesAllowlistEntry(url, entry)) {
return true;
}
}
Expand Down
Loading
Loading