mirror of
https://github.com/jamesstringer90/appsandbox.git
synced 2026-10-22 02:52:49 -05:00
Host-derived locale/keyboard/timezone/hostname + Linux cred validation + IDD audio fix
Linux guest provisioning now mirrors the host's regional settings instead
of hardcoding en_US / us / America/New_York / "ubuntu":
- detect_host_locale_settings() reads the current Windows user's locale
(GetUserDefaultLocaleName), keyboard (GetKeyboardLayoutList) and
timezone (GetDynamicTimeZoneInformation), translating each to the
Linux equivalent. Language is gated to the 8 ISO-preinstalled
languages (de/en/es/fr/it/pt/ru/zh) and falls back to en_US.UTF-8
otherwise; keyboard + timezone always match the host (every XKB
layout and IANA zone ships in the squashfs offline).
- VM name flows through as the guest hostname (mirrors Windows
ComputerName), staged as /etc/appsandbox-hostname.
- All four values staged as marker files; firstboot STEP 2/3/4 read
them with safe fallbacks and STEP 17 echoes the applied values.
Create-VM UI validation is now per-guest-OS, keyed consistently off
osType across validateVmName / validateUsername / validatePassword:
- Linux hostname: lowercase, RFC-1123 charset
- Linux username: ^[a-z_][a-z0-9_-]*$, <=32 chars
- Linux password: all chars allowed (host $6$ hash path), <=255 bytes
- default name lowercases to "myappsandbox" when Linux is selected
- OS Type moved above VM Name in the modal; .field-warn now wraps so
long messages don't break the modal layout; added admin-pass-warn span
IDD audio: drop the unused GetMixFormat call that returned
AUDCLNT_E_UNSUPPORTED_FORMAT on some AMD endpoints and needlessly killed
the audio path. renderfmt is built from the guest header and Initialize
runs with AUTOCONVERTPCM, so the host mix format was never used.
This commit is contained in:
1 parent
45285394d9
commit
4e45f8ef93
6 files changed
+404
-34
No files matched your search
+270
-2
@@ -1356,12 +1356,236 @@ static int stage_marker_file(FILE *manifest_f, const wchar_t *staging,
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ====================================================================
|
||||
* Host locale / keyboard / timezone detection + Windows->Linux mapping
|
||||
*
|
||||
* At VM-create time we read the *current user's* Windows regional
|
||||
* settings and translate them to the Ubuntu guest's equivalents, which
|
||||
* firstboot applies (replacing the old hardcoded en_US / us / New_York).
|
||||
*
|
||||
* Language is GATED: Ubuntu's ISO only ships pre-installed UI
|
||||
* translations for 8 languages (de, en, es, fr, it, pt, ru, zh). If the
|
||||
* host language isn't one of those, the locale falls back to en_US.UTF-8
|
||||
* so the guest never lands on a half-translated desktop. Keyboard and
|
||||
* timezone are NOT gated — every XKB layout and IANA zone is present in
|
||||
* the squashfs offline, so they always match the host.
|
||||
* ==================================================================== */
|
||||
|
||||
/* Map a BCP-47 tag (e.g. "en-GB", "zh-Hans-CN", "ja-JP") to a Linux
|
||||
* locale string ("en_GB.UTF-8"). Returns the en_US.UTF-8 fallback when
|
||||
* the primary language subtag is not one of the 8 preinstalled langs. */
|
||||
static void win_locale_to_linux(const wchar_t *bcp47, char *out, size_t out_sz)
|
||||
{
|
||||
static const wchar_t *supported[] = {
|
||||
L"de", L"en", L"es", L"fr", L"it", L"pt", L"ru", L"zh"
|
||||
};
|
||||
/* Default region per language when the host tag carries no region. */
|
||||
static const struct { const wchar_t *lang; const char *loc; } defloc[] = {
|
||||
{ L"de", "de_DE" }, { L"en", "en_US" }, { L"es", "es_ES" },
|
||||
{ L"fr", "fr_FR" }, { L"it", "it_IT" }, { L"pt", "pt_PT" },
|
||||
{ L"ru", "ru_RU" }, { L"zh", "zh_CN" },
|
||||
};
|
||||
|
||||
strcpy_s(out, out_sz, "en_US.UTF-8");
|
||||
if (!bcp47 || !bcp47[0]) return;
|
||||
|
||||
/* Split into up to 4 subtags. */
|
||||
wchar_t buf[64];
|
||||
wcsncpy_s(buf, 64, bcp47, _TRUNCATE);
|
||||
wchar_t lang[16] = L"", script[16] = L"", region[16] = L"";
|
||||
{
|
||||
wchar_t *ctx = NULL;
|
||||
wchar_t *tok = wcstok_s(buf, L"-", &ctx);
|
||||
if (tok) { wcsncpy_s(lang, 16, tok, _TRUNCATE); _wcslwr_s(lang, 16); }
|
||||
while ((tok = wcstok_s(NULL, L"-", &ctx)) != NULL) {
|
||||
size_t l = wcslen(tok);
|
||||
if (l == 4) { wcsncpy_s(script, 16, tok, _TRUNCATE); } /* script */
|
||||
else if (l == 2 || l == 3) { wcsncpy_s(region, 16, tok, _TRUNCATE);
|
||||
_wcsupr_s(region, 16); } /* region */
|
||||
}
|
||||
}
|
||||
if (!lang[0]) return;
|
||||
|
||||
/* Gate on the 8 supported languages. */
|
||||
int ok = 0;
|
||||
for (int i = 0; i < (int)(sizeof(supported)/sizeof(supported[0])); i++)
|
||||
if (_wcsicmp(lang, supported[i]) == 0) { ok = 1; break; }
|
||||
if (!ok) return; /* keep en_US.UTF-8 fallback */
|
||||
|
||||
/* Chinese needs script->region resolution (zh_CN vs zh_TW etc.). */
|
||||
if (_wcsicmp(lang, L"zh") == 0) {
|
||||
const char *loc = "zh_CN";
|
||||
if (region[0]) {
|
||||
if (!_wcsicmp(region, L"TW")) loc = "zh_TW";
|
||||
else if (!_wcsicmp(region, L"HK")) loc = "zh_HK";
|
||||
else if (!_wcsicmp(region, L"SG")) loc = "zh_SG";
|
||||
else loc = "zh_CN";
|
||||
} else if (script[0]) {
|
||||
loc = (!_wcsicmp(script, L"Hant")) ? "zh_TW" : "zh_CN";
|
||||
}
|
||||
strcpy_s(out, out_sz, loc);
|
||||
strcat_s(out, out_sz, ".UTF-8");
|
||||
return;
|
||||
}
|
||||
|
||||
if (region[0]) {
|
||||
char lang_a[16], region_a[16];
|
||||
WideCharToMultiByte(CP_UTF8, 0, lang, -1, lang_a, sizeof(lang_a), NULL, NULL);
|
||||
WideCharToMultiByte(CP_UTF8, 0, region, -1, region_a, sizeof(region_a), NULL, NULL);
|
||||
snprintf(out, out_sz, "%s_%s.UTF-8", lang_a, region_a);
|
||||
} else {
|
||||
for (int i = 0; i < (int)(sizeof(defloc)/sizeof(defloc[0])); i++)
|
||||
if (_wcsicmp(lang, defloc[i].lang) == 0) {
|
||||
snprintf(out, out_sz, "%s.UTF-8", defloc[i].loc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Map a Windows keyboard input-language id (LOWORD of an HKL) to an XKB
|
||||
* layout. Never gated — all layouts ship in xkb-data. Fallback "us". */
|
||||
static void langid_to_xkb(WORD langid, char *out, size_t out_sz)
|
||||
{
|
||||
static const struct { WORD id; const char *xkb; } map[] = {
|
||||
{ 0x0409, "us" }, { 0x0809, "gb" }, { 0x0c09, "us" }, /* en-AU->us */
|
||||
{ 0x0407, "de" }, { 0x0c07, "at" }, { 0x0807, "ch" },
|
||||
{ 0x040c, "fr" }, { 0x0c0c, "ca" }, { 0x080c, "be" }, { 0x100c, "ch" },
|
||||
{ 0x0410, "it" }, { 0x0810, "ch" },
|
||||
{ 0x040a, "es" }, { 0x080a, "latam" }, { 0x0c0a, "es" },
|
||||
{ 0x0416, "br" }, { 0x0816, "pt" },
|
||||
{ 0x0411, "jp" }, { 0x0412, "kr" },
|
||||
{ 0x0804, "cn" }, { 0x0404, "tw" }, { 0x0c04, "cn" }, { 0x1404, "cn" },
|
||||
{ 0x0419, "ru" }, { 0x0415, "pl" },
|
||||
{ 0x0413, "nl" }, { 0x0813, "be" },
|
||||
{ 0x041d, "se" }, { 0x0414, "no" }, { 0x0406, "dk" }, { 0x040b, "fi" },
|
||||
{ 0x0405, "cz" }, { 0x040e, "hu" }, { 0x041f, "tr" },
|
||||
{ 0x0401, "ara" }, { 0x040d, "il" }, { 0x041e, "th" },
|
||||
{ 0x0422, "ua" }, { 0x0418, "ro" }, { 0x0408, "gr" },
|
||||
{ 0x0402, "bg" }, { 0x041a, "hr" }, { 0x041b, "sk" }, { 0x0424, "si" },
|
||||
{ 0x0425, "ee" }, { 0x0426, "lv" }, { 0x0427, "lt" },
|
||||
};
|
||||
strcpy_s(out, out_sz, "us");
|
||||
for (int i = 0; i < (int)(sizeof(map)/sizeof(map[0])); i++)
|
||||
if (map[i].id == langid) { strcpy_s(out, out_sz, map[i].xkb); return; }
|
||||
/* Fall back on the primary language id (low 10 bits) for unlisted
|
||||
* sublang variants. */
|
||||
WORD prim = (WORD)(langid & 0x3ff);
|
||||
for (int i = 0; i < (int)(sizeof(map)/sizeof(map[0])); i++)
|
||||
if ((WORD)(map[i].id & 0x3ff) == prim) { strcpy_s(out, out_sz, map[i].xkb); return; }
|
||||
}
|
||||
|
||||
/* Map a Windows time-zone key name to an IANA zone. Never gated — all
|
||||
* zones ship in tzdata. Fallback "Etc/UTC". Subset of CLDR windowsZones
|
||||
* covering the common zones; unknown keys fall back to UTC. */
|
||||
static void win_tz_to_iana(const wchar_t *keyname, char *out, size_t out_sz)
|
||||
{
|
||||
static const struct { const wchar_t *win; const char *iana; } map[] = {
|
||||
{ L"Dateline Standard Time", "Etc/GMT+12" },
|
||||
{ L"Hawaiian Standard Time", "Pacific/Honolulu" },
|
||||
{ L"Alaskan Standard Time", "America/Anchorage" },
|
||||
{ L"Pacific Standard Time", "America/Los_Angeles" },
|
||||
{ L"US Mountain Standard Time", "America/Phoenix" },
|
||||
{ L"Mountain Standard Time", "America/Denver" },
|
||||
{ L"Central Standard Time", "America/Chicago" },
|
||||
{ L"Canada Central Standard Time", "America/Regina" },
|
||||
{ L"Central America Standard Time", "America/Guatemala" },
|
||||
{ L"Eastern Standard Time", "America/New_York" },
|
||||
{ L"US Eastern Standard Time", "America/Indiana/Indianapolis" },
|
||||
{ L"Atlantic Standard Time", "America/Halifax" },
|
||||
{ L"SA Pacific Standard Time", "America/Bogota" },
|
||||
{ L"SA Eastern Standard Time", "America/Cayenne" },
|
||||
{ L"E. South America Standard Time","America/Sao_Paulo" },
|
||||
{ L"Argentina Standard Time", "America/Buenos_Aires" },
|
||||
{ L"Greenwich Standard Time", "Atlantic/Reykjavik" },
|
||||
{ L"GMT Standard Time", "Europe/London" },
|
||||
{ L"W. Europe Standard Time", "Europe/Berlin" },
|
||||
{ L"Central Europe Standard Time", "Europe/Budapest" },
|
||||
{ L"Romance Standard Time", "Europe/Paris" },
|
||||
{ L"Central European Standard Time","Europe/Warsaw" },
|
||||
{ L"W. Central Africa Standard Time","Africa/Lagos" },
|
||||
{ L"FLE Standard Time", "Europe/Kiev" },
|
||||
{ L"GTB Standard Time", "Europe/Bucharest" },
|
||||
{ L"E. Europe Standard Time", "Europe/Chisinau" },
|
||||
{ L"Russian Standard Time", "Europe/Moscow" },
|
||||
{ L"Turkey Standard Time", "Europe/Istanbul" },
|
||||
{ L"Israel Standard Time", "Asia/Jerusalem" },
|
||||
{ L"Egypt Standard Time", "Africa/Cairo" },
|
||||
{ L"South Africa Standard Time", "Africa/Johannesburg" },
|
||||
{ L"Arabian Standard Time", "Asia/Dubai" },
|
||||
{ L"Arab Standard Time", "Asia/Riyadh" },
|
||||
{ L"Iran Standard Time", "Asia/Tehran" },
|
||||
{ L"Pakistan Standard Time", "Asia/Karachi" },
|
||||
{ L"India Standard Time", "Asia/Kolkata" },
|
||||
{ L"Bangladesh Standard Time", "Asia/Dhaka" },
|
||||
{ L"SE Asia Standard Time", "Asia/Bangkok" },
|
||||
{ L"China Standard Time", "Asia/Shanghai" },
|
||||
{ L"Singapore Standard Time", "Asia/Singapore" },
|
||||
{ L"Taipei Standard Time", "Asia/Taipei" },
|
||||
{ L"Tokyo Standard Time", "Asia/Tokyo" },
|
||||
{ L"Korea Standard Time", "Asia/Seoul" },
|
||||
{ L"W. Australia Standard Time", "Australia/Perth" },
|
||||
{ L"AUS Central Standard Time", "Australia/Darwin" },
|
||||
{ L"AUS Eastern Standard Time", "Australia/Sydney" },
|
||||
{ L"E. Australia Standard Time", "Australia/Brisbane" },
|
||||
{ L"New Zealand Standard Time", "Pacific/Auckland" },
|
||||
{ L"UTC", "Etc/UTC" },
|
||||
};
|
||||
strcpy_s(out, out_sz, "Etc/UTC");
|
||||
if (!keyname || !keyname[0]) return;
|
||||
for (int i = 0; i < (int)(sizeof(map)/sizeof(map[0])); i++)
|
||||
if (_wcsicmp(keyname, map[i].win) == 0) {
|
||||
strcpy_s(out, out_sz, map[i].iana); return;
|
||||
}
|
||||
}
|
||||
|
||||
/* Read the current user's locale, keyboard layout, and timezone from the
|
||||
* Windows host and translate to Linux equivalents. Best-effort: any
|
||||
* failure leaves the corresponding output at its safe default. */
|
||||
static void detect_host_locale_settings(char *locale, size_t locale_sz,
|
||||
char *xkb, size_t xkb_sz,
|
||||
char *tz, size_t tz_sz)
|
||||
{
|
||||
/* Locale (gated). */
|
||||
{
|
||||
wchar_t name[LOCALE_NAME_MAX_LENGTH] = L"";
|
||||
if (GetUserDefaultLocaleName(name, LOCALE_NAME_MAX_LENGTH) > 0)
|
||||
win_locale_to_linux(name, locale, locale_sz);
|
||||
else
|
||||
strcpy_s(locale, locale_sz, "en_US.UTF-8");
|
||||
asb_log(L"Host locale: %s -> %hs", name[0] ? name : L"(none)", locale);
|
||||
}
|
||||
/* Keyboard (not gated). */
|
||||
{
|
||||
HKL list[16];
|
||||
int n = GetKeyboardLayoutList(16, list);
|
||||
WORD langid = 0x0409;
|
||||
if (n > 0) langid = (WORD)((UINT_PTR)list[0] & 0xFFFF);
|
||||
langid_to_xkb(langid, xkb, xkb_sz);
|
||||
asb_log(L"Host keyboard: langid=0x%04x -> %hs", langid, xkb);
|
||||
}
|
||||
/* Timezone (not gated). */
|
||||
{
|
||||
DYNAMIC_TIME_ZONE_INFORMATION dtzi;
|
||||
DWORD r = GetDynamicTimeZoneInformation(&dtzi);
|
||||
if (r != TIME_ZONE_ID_INVALID && dtzi.TimeZoneKeyName[0])
|
||||
win_tz_to_iana(dtzi.TimeZoneKeyName, tz, tz_sz);
|
||||
else
|
||||
strcpy_s(tz, tz_sz, "Etc/UTC");
|
||||
asb_log(L"Host timezone: %s -> %hs",
|
||||
(r != TIME_ZONE_ID_INVALID) ? dtzi.TimeZoneKeyName : L"(invalid)", tz);
|
||||
}
|
||||
}
|
||||
|
||||
static int generate_vhdx_manifest_ubuntu(const wchar_t *manifest_path,
|
||||
const wchar_t *staging,
|
||||
const wchar_t *res_dir,
|
||||
BOOL ssh_enabled,
|
||||
const wchar_t *admin_user,
|
||||
const char *admin_pw_hash)
|
||||
const char *admin_pw_hash,
|
||||
const char *host_locale,
|
||||
const char *host_xkb,
|
||||
const char *host_tz,
|
||||
const wchar_t *vm_name)
|
||||
{
|
||||
wchar_t extras[MAX_PATH];
|
||||
CreateDirectoryW(staging, NULL);
|
||||
@@ -1407,6 +1631,40 @@ static int generate_vhdx_manifest_ubuntu(const wchar_t *manifest_path,
|
||||
NULL, 0, L"/etc/appsandbox-ssh-enabled");
|
||||
asb_log(L"ssh_enabled: staged /etc/appsandbox-ssh-enabled marker");
|
||||
}
|
||||
|
||||
/* Host-derived locale / keyboard / timezone. firstboot STEP 3 + 4
|
||||
* read these; absent markers fall back to en_US.UTF-8 / us / Etc/UTC. */
|
||||
if (host_locale && host_locale[0])
|
||||
n += stage_marker_file(f, staging, L"locale.marker",
|
||||
host_locale, strlen(host_locale),
|
||||
L"/etc/appsandbox-locale");
|
||||
if (host_xkb && host_xkb[0])
|
||||
n += stage_marker_file(f, staging, L"keyboard.marker",
|
||||
host_xkb, strlen(host_xkb),
|
||||
L"/etc/appsandbox-keyboard");
|
||||
if (host_tz && host_tz[0])
|
||||
n += stage_marker_file(f, staging, L"timezone.marker",
|
||||
host_tz, strlen(host_tz),
|
||||
L"/etc/appsandbox-timezone");
|
||||
asb_log(L"Host regional markers staged: locale=%hs keyboard=%hs tz=%hs",
|
||||
host_locale ? host_locale : "(none)",
|
||||
host_xkb ? host_xkb : "(none)",
|
||||
host_tz ? host_tz : "(none)");
|
||||
|
||||
/* Hostname from the AppSandbox VM name (mirrors the Windows
|
||||
* ComputerName path). The web UI already validates the name to a
|
||||
* legal lowercase RFC-1123 hostname, so we write it verbatim;
|
||||
* firstboot STEP 2 reads it and falls back to "ubuntu" if absent. */
|
||||
if (vm_name && vm_name[0]) {
|
||||
char host_utf8[256];
|
||||
WideCharToMultiByte(CP_UTF8, 0, vm_name, -1,
|
||||
host_utf8, sizeof(host_utf8), NULL, NULL);
|
||||
n += stage_marker_file(f, staging, L"hostname.marker",
|
||||
host_utf8, strlen(host_utf8),
|
||||
L"/etc/appsandbox-hostname");
|
||||
asb_log(L"Linux hostname: staged /etc/appsandbox-hostname = %s", vm_name);
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
asb_log(L"Linux staging manifest written: %s (%d file(s))", manifest_path, n);
|
||||
return n;
|
||||
@@ -1871,10 +2129,20 @@ static DWORD WINAPI linux_create_thread(LPVOID param)
|
||||
}
|
||||
SecureZeroMemory(args->config.admin_pass, sizeof(args->config.admin_pass));
|
||||
|
||||
/* Read the current user's Windows regional settings and translate to
|
||||
* Linux. Language is gated to the 8 ISO-preinstalled languages
|
||||
* (else en_US.UTF-8); keyboard + timezone always match the host. */
|
||||
char host_locale[64], host_xkb[32], host_tz[64];
|
||||
detect_host_locale_settings(host_locale, sizeof(host_locale),
|
||||
host_xkb, sizeof(host_xkb),
|
||||
host_tz, sizeof(host_tz));
|
||||
|
||||
int n_staged = generate_vhdx_manifest_ubuntu(manifest, staging, res_dir,
|
||||
args->config.ssh_enabled,
|
||||
args->config.admin_user,
|
||||
admin_pw_hash);
|
||||
admin_pw_hash,
|
||||
host_locale, host_xkb, host_tz,
|
||||
args->config.name);
|
||||
SecureZeroMemory(admin_pw_hash, sizeof(admin_pw_hash));
|
||||
if (n_staged < 0) {
|
||||
args->result = E_FAIL;
|
||||
|
||||
@@ -819,7 +819,6 @@ static DWORD WINAPI audio_recv_thread_proc(LPVOID param)
|
||||
IMMDevice *pDev = NULL;
|
||||
IAudioClient *pAC = NULL;
|
||||
IAudioRenderClient *pRC = NULL;
|
||||
WAVEFORMATEX *mixfmt = NULL;
|
||||
WAVEFORMATEX *renderfmt = NULL;
|
||||
BYTE *scratch = NULL;
|
||||
UINT32 scratch_cap = 0;
|
||||
@@ -882,10 +881,13 @@ static DWORD WINAPI audio_recv_thread_proc(LPVOID param)
|
||||
hr = IMMDevice_Activate(pDev, &AUDIO_IID_IAudioClient, CLSCTX_ALL, NULL, (void **)&pAC);
|
||||
if (FAILED(hr)) { idd_log(d, L"Audio: Activate failed 0x%08lX.", hr); goto session_cleanup; }
|
||||
|
||||
hr = IAudioClient_GetMixFormat(pAC, &mixfmt);
|
||||
if (FAILED(hr) || !mixfmt) { idd_log(d, L"Audio: GetMixFormat failed 0x%08lX.", hr); goto session_cleanup; }
|
||||
|
||||
/* Build the source format exactly matching what the guest is sending. */
|
||||
/* Build the source format exactly matching what the guest is sending.
|
||||
We deliberately do NOT call GetMixFormat: we never use the host's
|
||||
mix format (Initialize runs with AUTOCONVERTPCM, so WASAPI converts
|
||||
our renderfmt to whatever the endpoint needs). On some endpoints
|
||||
(seen on AMD HD Audio) GetMixFormat returns
|
||||
AUDCLNT_E_UNSUPPORTED_FORMAT for the configured default format,
|
||||
which would needlessly kill the whole audio path. */
|
||||
renderfmt = (WAVEFORMATEX *)CoTaskMemAlloc(sizeof(WAVEFORMATEX));
|
||||
if (!renderfmt) goto session_cleanup;
|
||||
ZeroMemory(renderfmt, sizeof(WAVEFORMATEX));
|
||||
@@ -975,7 +977,6 @@ session_cleanup:
|
||||
if (pAC) { IAudioClient_Release(pAC); pAC = NULL; }
|
||||
if (pDev) { IMMDevice_Release(pDev); pDev = NULL; }
|
||||
if (pEnum) { IMMDeviceEnumerator_Release(pEnum); pEnum = NULL; }
|
||||
if (mixfmt) { CoTaskMemFree(mixfmt); mixfmt = NULL; }
|
||||
if (renderfmt) { CoTaskMemFree(renderfmt); renderfmt = NULL; }
|
||||
|
||||
if (d->audio_socket != INVALID_SOCKET) {
|
||||
|
||||
@@ -1114,31 +1114,64 @@ static void plant_firstboot_service(ext4_writer_t *ew)
|
||||
" echo \"SKIP STEP 1: grub-install binary not found in rootfs\"\n"
|
||||
"fi\n"
|
||||
"\n"
|
||||
"# --- STEP 2: Hostname ---\n"
|
||||
"# --- STEP 2: Hostname (from host marker = AppSandbox VM name) ---\n"
|
||||
"# Host staged /etc/appsandbox-hostname with the VM name (already\n"
|
||||
"# validated to a legal lowercase hostname by the create UI). Falls\n"
|
||||
"# back to 'ubuntu' if the marker is absent.\n"
|
||||
"echo \"==== STEP 2: hostname ====\"\n"
|
||||
"echo ubuntu > /etc/hostname && echo \"OK: /etc/hostname\" || echo \"FAIL\"\n"
|
||||
"command -v hostnamectl >/dev/null 2>&1 && hostnamectl set-hostname ubuntu || echo \"SKIP hostnamectl\"\n"
|
||||
"HN=ubuntu\n"
|
||||
"if [ -s /etc/appsandbox-hostname ]; then HN=$(tr -d '\\r\\n[:space:]' < /etc/appsandbox-hostname); fi\n"
|
||||
"[ -n \"$HN\" ] || HN=ubuntu\n"
|
||||
"echo \"$HN\" > /etc/hostname && echo \"OK: /etc/hostname=$HN\" || echo \"FAIL\"\n"
|
||||
"command -v hostnamectl >/dev/null 2>&1 && hostnamectl set-hostname \"$HN\" || echo \"SKIP hostnamectl\"\n"
|
||||
"if [ -f /etc/hosts ]; then\n"
|
||||
" sed -i 's/^127\\.0\\.1\\.1.*/127.0.1.1\\tubuntu/' /etc/hosts || true\n"
|
||||
" grep -q '^127.0.1.1' /etc/hosts || printf '127.0.1.1\\tubuntu\\n' >> /etc/hosts\n"
|
||||
" sed -i \"s/^127\\.0\\.1\\.1.*/127.0.1.1\\t$HN/\" /etc/hosts || true\n"
|
||||
" grep -q '^127.0.1.1' /etc/hosts || printf '127.0.1.1\\t%s\\n' \"$HN\" >> /etc/hosts\n"
|
||||
"fi\n"
|
||||
"rm -f /etc/appsandbox-hostname 2>/dev/null || true\n"
|
||||
"\n"
|
||||
"# --- STEP 3: Timezone (US Eastern) ---\n"
|
||||
"# --- STEP 3: Timezone (from host marker, fallback Etc/UTC) ---\n"
|
||||
"# Host staged /etc/appsandbox-timezone with the IANA zone derived\n"
|
||||
"# from the current Windows user's timezone. All zones ship in\n"
|
||||
"# tzdata so any value resolves offline.\n"
|
||||
"echo \"==== STEP 3: timezone ====\"\n"
|
||||
"ln -sf /usr/share/zoneinfo/America/New_York /etc/localtime && echo \"OK: localtime\" || echo \"FAIL\"\n"
|
||||
"echo America/New_York > /etc/timezone || true\n"
|
||||
"TZ=Etc/UTC\n"
|
||||
"if [ -s /etc/appsandbox-timezone ]; then TZ=$(tr -d '\\r\\n[:space:]' < /etc/appsandbox-timezone); fi\n"
|
||||
"if [ ! -e \"/usr/share/zoneinfo/$TZ\" ]; then echo \"WARN: zoneinfo $TZ missing, using Etc/UTC\"; TZ=Etc/UTC; fi\n"
|
||||
"ln -sf \"/usr/share/zoneinfo/$TZ\" /etc/localtime && echo \"OK: localtime=$TZ\" || echo \"FAIL: localtime\"\n"
|
||||
"echo \"$TZ\" > /etc/timezone || true\n"
|
||||
"rm -f /etc/appsandbox-timezone 2>/dev/null || true\n"
|
||||
"\n"
|
||||
"# --- STEP 4: Locale + keyboard ---\n"
|
||||
"# --- STEP 4: Locale + keyboard (from host markers, gated fallbacks) ---\n"
|
||||
"# Host staged /etc/appsandbox-locale (already gated to the 8\n"
|
||||
"# ISO-preinstalled languages, else en_US.UTF-8) and\n"
|
||||
"# /etc/appsandbox-keyboard (XKB layout). If locale-gen rejects the\n"
|
||||
"# locale we fall back to en_US.UTF-8 so the desktop is never broken.\n"
|
||||
"echo \"==== STEP 4: locale + keyboard ====\"\n"
|
||||
"echo 'LANG=en_US.UTF-8' > /etc/default/locale || true\n"
|
||||
"command -v locale-gen >/dev/null 2>&1 && locale-gen en_US.UTF-8 >/dev/null 2>&1 || echo \"SKIP locale-gen\"\n"
|
||||
"cat > /etc/default/keyboard <<'EOF'\n"
|
||||
"LOC=en_US.UTF-8\n"
|
||||
"if [ -s /etc/appsandbox-locale ]; then LOC=$(tr -d '\\r\\n[:space:]' < /etc/appsandbox-locale); fi\n"
|
||||
"echo \"LANG=$LOC\" > /etc/default/locale || true\n"
|
||||
"if command -v locale-gen >/dev/null 2>&1; then\n"
|
||||
" if ! locale-gen \"$LOC\" >/dev/null 2>&1; then\n"
|
||||
" echo \"WARN: locale-gen $LOC failed, falling back to en_US.UTF-8\"\n"
|
||||
" LOC=en_US.UTF-8; echo \"LANG=$LOC\" > /etc/default/locale\n"
|
||||
" locale-gen en_US.UTF-8 >/dev/null 2>&1 || true\n"
|
||||
" fi\n"
|
||||
" echo \"OK: locale=$LOC\"\n"
|
||||
"else\n"
|
||||
" echo \"SKIP locale-gen\"\n"
|
||||
"fi\n"
|
||||
"KB=us\n"
|
||||
"if [ -s /etc/appsandbox-keyboard ]; then KB=$(tr -d '\\r\\n[:space:]' < /etc/appsandbox-keyboard); fi\n"
|
||||
"cat > /etc/default/keyboard <<EOF\n"
|
||||
"XKBMODEL=\"pc105\"\n"
|
||||
"XKBLAYOUT=\"us\"\n"
|
||||
"XKBLAYOUT=\"$KB\"\n"
|
||||
"XKBVARIANT=\"\"\n"
|
||||
"XKBOPTIONS=\"\"\n"
|
||||
"BACKSPACE=\"guess\"\n"
|
||||
"EOF\n"
|
||||
"echo \"OK: keyboard=$KB\"\n"
|
||||
"rm -f /etc/appsandbox-locale /etc/appsandbox-keyboard 2>/dev/null || true\n"
|
||||
"\n"
|
||||
"# --- STEP 5: User account (from create-VM modal, falls back to test) ---\n"
|
||||
"# Host writes the chosen username + glibc $6$ hash into these\n"
|
||||
@@ -1604,6 +1637,11 @@ static void plant_firstboot_service(ext4_writer_t *ew)
|
||||
"echo \"-- grub cmdline drop-in --\"\n"
|
||||
"_check_file /etc/default/grub.d/99-appsandbox-no-efifb.cfg\n"
|
||||
"_check_glob '/opt/appsandbox/local-apt/pool/main/*'\n"
|
||||
"echo \"-- regional settings (from host) --\"\n"
|
||||
"echo \" hostname: $(cat /etc/hostname 2>/dev/null)\"\n"
|
||||
"echo \" locale: $(grep -h LANG= /etc/default/locale 2>/dev/null | head -1)\"\n"
|
||||
"echo \" keyboard: $(grep -h XKBLAYOUT /etc/default/keyboard 2>/dev/null | head -1)\"\n"
|
||||
"echo \" timezone: $(cat /etc/timezone 2>/dev/null)\"\n"
|
||||
"set -x\n"
|
||||
"\n"
|
||||
"# --- STEP 99: Mark done + reboot ---\n"
|
||||
|
||||
+65
-6
@@ -111,7 +111,15 @@ function applyOsTypeUI() {
|
||||
hidden so the cloud-image code path can be revived without
|
||||
re-adding the markup. */
|
||||
for (var k = 0; k < needsLinuxVersion.length; k++) needsLinuxVersion[k].style.display = 'none';
|
||||
/* Swap the default VM name between OS conventions, but only when the
|
||||
field still holds the *other* OS's untouched default — never clobber a
|
||||
name the user typed. Linux hostnames must be lowercase. */
|
||||
var nameEl = document.getElementById('vm-name');
|
||||
if (isLinux && nameEl.value === 'MyAppSandbox') nameEl.value = 'myappsandbox';
|
||||
else if (!isLinux && nameEl.value === 'myappsandbox') nameEl.value = 'MyAppSandbox';
|
||||
revalidateVmName();
|
||||
revalidateUsername();
|
||||
revalidatePassword();
|
||||
updateCreateButtons();
|
||||
}
|
||||
|
||||
@@ -351,9 +359,16 @@ function revalidateVmName() {
|
||||
document.getElementById('vm-name-warn').textContent = validateVmName(name) || '';
|
||||
}
|
||||
document.getElementById('vm-name').addEventListener('input', revalidateVmName);
|
||||
document.getElementById('admin-user').addEventListener('input', function() {
|
||||
document.getElementById('admin-user-warn').textContent = validateUsername(this.value.trim()) || '';
|
||||
});
|
||||
|
||||
function revalidateUsername() {
|
||||
var u = document.getElementById('admin-user').value.trim();
|
||||
document.getElementById('admin-user-warn').textContent = validateUsername(u) || '';
|
||||
}
|
||||
function revalidatePassword() {
|
||||
var p = document.getElementById('admin-pass').value;
|
||||
document.getElementById('admin-pass-warn').textContent = validatePassword(p) || '';
|
||||
}
|
||||
document.getElementById('admin-user').addEventListener('input', revalidateUsername);
|
||||
|
||||
function checkPasswordMatch() {
|
||||
var pass = document.getElementById('admin-pass').value;
|
||||
@@ -370,7 +385,10 @@ function checkPasswordMatch() {
|
||||
confirm.classList.add('pass-mismatch');
|
||||
}
|
||||
}
|
||||
document.getElementById('admin-pass').addEventListener('input', checkPasswordMatch);
|
||||
document.getElementById('admin-pass').addEventListener('input', function() {
|
||||
checkPasswordMatch();
|
||||
revalidatePassword();
|
||||
});
|
||||
document.getElementById('admin-confirm').addEventListener('input', checkPasswordMatch);
|
||||
checkPasswordMatch();
|
||||
|
||||
@@ -424,15 +442,19 @@ function clearCreateForm() {
|
||||
updateCreateButtons();
|
||||
}
|
||||
|
||||
/* VM name / hostname validation. Per-guest-OS rules, keyed off the
|
||||
selected OS Type (on a macOS host the dropdown is locked to 'macOS',
|
||||
so osType is an accurate guest discriminator on all hosts). */
|
||||
function validateVmName(name) {
|
||||
if (!name) return 'VM name is required.';
|
||||
var osSelect = document.getElementById('os-type');
|
||||
var osType = osSelect ? osSelect.value : 'Windows';
|
||||
if (hostBridge.isMac) {
|
||||
if (osType === 'macOS') {
|
||||
if (name.length > 63) return 'VM name cannot exceed 63 characters (macOS LocalHostName limit).';
|
||||
} else if (osType === 'Linux') {
|
||||
if (name.length > 63) return 'VM name cannot exceed 63 characters (Linux hostname limit).';
|
||||
} else {
|
||||
if (/[A-Z]/.test(name)) return 'Linux hostname must be lowercase.';
|
||||
} else { /* Windows */
|
||||
if (name.length > 15) return 'VM name cannot exceed 15 characters (NetBIOS limit).';
|
||||
}
|
||||
if (/[^a-zA-Z0-9-]/.test(name)) return 'VM name can only contain letters, digits, and hyphens.';
|
||||
@@ -448,8 +470,23 @@ function validateVmName(name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/* Username validation. Per-guest-OS rules keyed off osType. Each branch
|
||||
is explicit so it's clear which OS's account rules apply. */
|
||||
function validateUsername(name) {
|
||||
if (!name) return 'Username is required.';
|
||||
var osSelect = document.getElementById('os-type');
|
||||
var osType = osSelect ? osSelect.value : 'Windows';
|
||||
if (osType === 'Linux') {
|
||||
/* Ubuntu useradd/adduser: lowercase, start with a letter or
|
||||
underscore, then [a-z0-9_-], max 32 chars. */
|
||||
if (name.length > 32) return 'Username cannot exceed 32 characters (Linux limit).';
|
||||
if (!/^[a-z_][a-z0-9_-]*$/.test(name))
|
||||
return 'Lowercase alphanumeric only.';
|
||||
return null;
|
||||
}
|
||||
/* macOS and Windows: keep the existing Windows-account ruleset.
|
||||
(macOS-specific shortname rules are not yet verified; treated the
|
||||
same as Windows for now — see validatePassword note.) */
|
||||
if (name.length > 20) return 'Username cannot exceed 20 characters.';
|
||||
if (/["\\/\[\]:;|=,+*?<>]/.test(name)) return 'Username contains invalid characters.';
|
||||
if (/^[.\s]+$/.test(name)) return 'Username cannot be only dots or spaces.';
|
||||
@@ -461,12 +498,33 @@ function validateUsername(name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/* Password validation. Per-guest-OS rules keyed off osType.
|
||||
- Linux: Ubuntu accepts ALL characters via the host's $6$ hash path
|
||||
(usermod -p bypasses pwquality), so the only limits are non-empty
|
||||
and a sane byte ceiling.
|
||||
- macOS / Windows: no extra content rule enforced here today. */
|
||||
function validatePassword(pass) {
|
||||
var osSelect = document.getElementById('os-type');
|
||||
var osType = osSelect ? osSelect.value : 'Windows';
|
||||
if (osType === 'Linux') {
|
||||
if (!pass) return 'Password is required.';
|
||||
/* UTF-8 byte length (encodeURIComponent escapes multibyte). */
|
||||
var bytes = unescape(encodeURIComponent(pass)).length;
|
||||
if (bytes > 255) return 'Password is too long (max 255 bytes).';
|
||||
return null;
|
||||
}
|
||||
/* macOS / Windows: no additional constraints today. */
|
||||
return null;
|
||||
}
|
||||
|
||||
function onCreateVm() {
|
||||
var cfg = gatherConfig();
|
||||
var nameErr = validateVmName(cfg.name);
|
||||
if (nameErr) { sendCmd('log', { message: nameErr }); return; }
|
||||
var userErr = validateUsername(cfg.adminUser);
|
||||
if (userErr) { sendCmd('log', { message: userErr }); return; }
|
||||
var passErr = validatePassword(cfg.adminPass);
|
||||
if (passErr) { sendCmd('log', { message: passErr }); return; }
|
||||
if (cfg.adminPass !== cfg.adminConfirm) {
|
||||
sendCmd('log', { message: 'Passwords do not match.' });
|
||||
return;
|
||||
@@ -514,6 +572,7 @@ function openCreateModal() {
|
||||
/* Clear validation state */
|
||||
document.getElementById('vm-name-warn').textContent = '';
|
||||
document.getElementById('admin-user-warn').textContent = '';
|
||||
document.getElementById('admin-pass-warn').textContent = '';
|
||||
checkPasswordMatch();
|
||||
onNetModeChange();
|
||||
applyOsTypeUI(); /* fires updateCreateButtons + revalidateVmName */
|
||||
|
||||
+7
-6
@@ -63,12 +63,6 @@
|
||||
<div class="modal wide">
|
||||
<h3>New Sandbox</h3>
|
||||
<div class="form-grid">
|
||||
<label for="vm-name">VM Name:</label>
|
||||
<div class="form-row">
|
||||
<input type="text" id="vm-name" value="MyAppSandbox" class="input-md">
|
||||
<span class="field-warn" id="vm-name-warn"></span>
|
||||
</div>
|
||||
|
||||
<label for="os-type">OS Type:</label>
|
||||
<div class="form-row">
|
||||
<select id="os-type" onchange="applyOsTypeUI()">
|
||||
@@ -78,6 +72,12 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<label for="vm-name">VM Name:</label>
|
||||
<div class="form-row">
|
||||
<input type="text" id="vm-name" value="MyAppSandbox" class="input-md">
|
||||
<span class="field-warn" id="vm-name-warn"></span>
|
||||
</div>
|
||||
|
||||
<label class="needs-iso">OS Image:</label>
|
||||
<div class="image-row needs-iso">
|
||||
<input type="text" id="image-path" placeholder="Path to ISO or VHDX...">
|
||||
@@ -150,6 +150,7 @@
|
||||
<label>Confirm:</label>
|
||||
<input type="password" id="admin-confirm" value="test123">
|
||||
<span class="toggle-pass" onmousedown="showPassword()" onmouseup="hidePassword()" onmouseleave="hidePassword()">👁</span>
|
||||
<span class="field-warn" id="admin-pass-warn"></span>
|
||||
</div>
|
||||
|
||||
<label>Options:</label>
|
||||
|
||||
+4
-1
@@ -223,7 +223,10 @@ button.primary:disabled { background: #2a3a4a; border-color: #3a4a5a; }
|
||||
.field-warn {
|
||||
font-size: 11px;
|
||||
color: var(--danger);
|
||||
white-space: nowrap;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Password match validation */
|
||||
|
||||
Reference in new issue
Block a user