Compare commits

...

6 Commits

Author SHA1 Message Date
Michel Rögl-Brunner
4628e67e5c Fix PBS certificate validation: pass PBS_FINGERPRINT, optional fingerprint for trusted CA
- Pass stored pbs_fingerprint as PBS_FINGERPRINT in login, snapshot list, and restore
- Allow empty fingerprint so trusted-CA PBS works without entering one
- Make fingerprint field optional in PBSCredentialsModal with updated helper text

Fixes #438
2026-01-29 13:55:53 +01:00
Michel Roegl-Brunner
578fa28461 Merge pull request #474 from community-scripts/fix/404
fix: allow domain names for APT Cacher in container creation UI
2026-01-29 13:42:31 +01:00
Michel Rögl-Brunner
9e6154b0de fix: allow domain names for APT Cacher in container creation UI
- Add validateHostname and validateAptCacherAddress (IPv4 or hostname)
- Use new validator for var_apt_cacher_ip; error message: Invalid IPv4 or hostname
- Label: APT Cacher host or IP; placeholder shows IP or hostname example

Fixes #404
2026-01-29 13:40:19 +01:00
Michel Roegl-Brunner
d29f71a92f Merge pull request #473 from community-scripts/fix/365
fix: detect app slug from LXC /usr/bin/update for port lookup
2026-01-29 13:28:36 +01:00
Michel Rögl-Brunner
aea14cda7e fix: detect app slug from LXC /usr/bin/update for port lookup
Resolve interface_port from community-scripts update file when hostname
differs from JSON slug (e.g. lxcpeanut vs peanut). Primary: slug parsed
from pct exec ... cat /usr/bin/update; fallback: hostname/suffix match.

Fixes #365
2026-01-29 13:26:29 +01:00
Michel Roegl-Brunner
4893ccda6e Merge pull request #472 from community-scripts/feat/406
feat: private/custom git repos - GitHub, GitLab, Bitbucket, custom
2026-01-29 13:11:54 +01:00
5 changed files with 97 additions and 34 deletions

View File

@@ -199,6 +199,17 @@ export function ConfigurationModal({
return !isNaN(num) && num > 0;
};
const validateHostname = (hostname: string): boolean => {
if (!hostname || hostname.length > 253) return false;
const label = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
const labels = hostname.split('.');
return labels.length >= 1 && labels.every(l => l.length >= 1 && l.length <= 63 && label.test(l));
};
const validateAptCacherAddress = (value: string): boolean => {
return validateIPv4(value) || validateHostname(value);
};
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
@@ -216,8 +227,8 @@ export function ConfigurationModal({
if (advancedVars.var_ns && !validateIPv4(advancedVars.var_ns as string)) {
newErrors.var_ns = 'Invalid IPv4 address';
}
if (advancedVars.var_apt_cacher_ip && !validateIPv4(advancedVars.var_apt_cacher_ip as string)) {
newErrors.var_apt_cacher_ip = 'Invalid IPv4 address';
if (advancedVars.var_apt_cacher_ip && !validateAptCacherAddress(advancedVars.var_apt_cacher_ip as string)) {
newErrors.var_apt_cacher_ip = 'Invalid IPv4 address or hostname';
}
// Validate IPv4 CIDR if network mode is static
const netValue = advancedVars.var_net;
@@ -904,13 +915,13 @@ export function ConfigurationModal({
</div>
<div>
<label className="block text-sm font-medium text-foreground mb-2">
APT Cacher IP
APT Cacher host or IP
</label>
<Input
type="text"
value={typeof advancedVars.var_apt_cacher_ip === 'boolean' ? '' : String(advancedVars.var_apt_cacher_ip ?? '')}
onChange={(e) => updateAdvancedVar('var_apt_cacher_ip', e.target.value)}
placeholder="192.168.1.10"
placeholder="192.168.1.10 or apt-cacher.internal"
className={errors.var_apt_cacher_ip ? 'border-destructive' : ''}
/>
{errors.var_apt_cacher_ip && (

View File

@@ -270,22 +270,21 @@ export function PBSCredentialsModal({
htmlFor="pbs-fingerprint"
className="text-foreground mb-1 block text-sm font-medium"
>
Fingerprint <span className="text-error">*</span>
Fingerprint
</label>
<input
type="text"
id="pbs-fingerprint"
value={pbsFingerprint}
onChange={(e) => setPbsFingerprint(e.target.value)}
required
disabled={isLoading}
className="bg-card text-foreground placeholder-muted-foreground focus:ring-ring focus:border-ring border-border w-full rounded-md border px-3 py-2 shadow-sm focus:ring-2 focus:outline-none"
placeholder="e.g., 7b:e5:87:38:5e:16:05:d1:12:22:7f:73:d2:e2:d0:cf:8c:cb:28:e2:74:0c:78:91:1a:71:74:2e:79:20:5a:02"
/>
<p className="text-muted-foreground mt-1 text-xs">
Server fingerprint for auto-acceptance. You can find this on
your PBS dashboard by clicking the &quot;Show Fingerprint&quot;
button.
Leave empty if PBS uses a trusted CA (e.g. Let&apos;s Encrypt).
For self-signed certificates, enter the server fingerprint from
the PBS dashboard (&quot;Show Fingerprint&quot;).
</p>
</div>

View File

@@ -2068,32 +2068,72 @@ export const installedScriptsRouter = createTRPCRouter({
};
}
// Get the script's interface_port from metadata (prioritize metadata over existing database values)
let detectedPort = 80; // Default fallback
// Resolve app slug from /usr/bin/update (community-scripts) when available; else from hostname/suffix.
let slugFromUpdate: string | null = null;
try {
const updateCommand = `pct exec ${scriptData.container_id} -- cat /usr/bin/update 2>/dev/null`;
let updateOutput = '';
await new Promise<void>((resolve) => {
void sshExecutionService.executeCommand(
server as Server,
updateCommand,
(data: string) => { updateOutput += data; },
() => {},
() => resolve()
);
});
const ctSlugMatch = /ct\/([a-zA-Z0-9_.-]+)\.sh/.exec(updateOutput);
if (ctSlugMatch?.[1]) {
slugFromUpdate = ctSlugMatch[1].trim().toLowerCase();
console.log('🔍 Slug from /usr/bin/update:', slugFromUpdate);
}
} catch {
// Container may not be from community-scripts; use hostname fallback
}
// Get the script's interface_port from metadata. Primary: slug from /usr/bin/update; fallback: hostname/suffix.
let detectedPort = 80; // Default fallback
try {
// Import localScriptsService to get script metadata
const { localScriptsService } = await import('~/server/services/localScripts');
// Get all scripts and find the one matching our script name
const allScripts = await localScriptsService.getAllScripts();
// Extract script slug from script_name (remove .sh extension)
const scriptSlug = scriptData.script_name.replace(/\.sh$/, '');
console.log('🔍 Looking for script with slug:', scriptSlug);
const scriptMetadata = allScripts.find(script => script.slug === scriptSlug);
const nameFromHostname = scriptData.script_name.replace(/\.sh$/, '').toLowerCase();
// Primary: slug from /usr/bin/update (community-scripts)
let scriptMetadata =
slugFromUpdate != null
? allScripts.find((s) => s.slug === slugFromUpdate)
: undefined;
if (scriptMetadata) {
console.log('🔍 Using slug from /usr/bin/update for metadata:', scriptMetadata.slug);
}
// Fallback: exact hostname then hostname ends with slug (longest wins)
if (!scriptMetadata) {
scriptMetadata = allScripts.find((script) => script.slug === nameFromHostname);
if (!scriptMetadata) {
const suffixMatches = allScripts.filter((script) => nameFromHostname.endsWith(script.slug));
scriptMetadata =
suffixMatches.length > 0
? suffixMatches.reduce((a, b) => (a.slug.length >= b.slug.length ? a : b))
: undefined;
if (scriptMetadata) {
console.log('🔍 Matched metadata by slug suffix in hostname:', scriptMetadata.slug);
}
}
}
if (scriptMetadata?.interface_port) {
detectedPort = scriptMetadata.interface_port;
console.log('📋 Found interface_port in metadata:', detectedPort);
} else {
console.log('📋 No interface_port found in metadata, using default port 80');
detectedPort = 80; // Default to port 80 if no metadata port found
detectedPort = 80;
}
} catch (error) {
console.log('⚠️ Error getting script metadata, using default port 80:', error);
detectedPort = 80; // Default to port 80 if metadata lookup fails
detectedPort = 80;
}
console.log('🎯 Final detected port:', detectedPort);

View File

@@ -327,13 +327,16 @@ class BackupService {
// PBS supports PBS_PASSWORD and PBS_REPOSITORY environment variables for non-interactive login
const repository = `root@pam@${pbsIp}:${pbsDatastore}`;
// Escape password for shell safety (single quotes)
// Escape password and fingerprint for shell safety (single quotes)
const escapedPassword = credential.pbs_password.replace(/'/g, "'\\''");
// Use PBS_PASSWORD environment variable for non-interactive authentication
// Auto-accept fingerprint by piping "y" to stdin
// PBS will use PBS_PASSWORD env var if available, avoiding interactive prompt
const fullCommand = `echo "y" | PBS_PASSWORD='${escapedPassword}' PBS_REPOSITORY='${repository}' timeout 10 proxmox-backup-client login --repository ${repository} 2>&1`;
const fingerprint = credential.pbs_fingerprint?.trim() ?? '';
const escapedFingerprint = fingerprint ? fingerprint.replace(/'/g, "'\\''") : '';
const envParts = [`PBS_PASSWORD='${escapedPassword}'`, `PBS_REPOSITORY='${repository}'`];
if (escapedFingerprint) {
envParts.push(`PBS_FINGERPRINT='${escapedFingerprint}'`);
}
const envStr = envParts.join(' ');
const fullCommand = `${envStr} timeout 10 proxmox-backup-client login --repository ${repository} 2>&1`;
console.log(`[BackupService] Logging into PBS: ${repository}`);
@@ -419,9 +422,12 @@ class BackupService {
// Build full repository string: root@pam@<IP>:<DATASTORE>
const repository = `root@pam@${pbsIp}:${pbsDatastore}`;
const fingerprint = credential.pbs_fingerprint?.trim() ?? '';
const escapedFingerprint = fingerprint ? fingerprint.replace(/'/g, "'\\''") : '';
const snapshotEnvParts = escapedFingerprint ? [`PBS_FINGERPRINT='${escapedFingerprint}'`] : [];
const snapshotEnvStr = snapshotEnvParts.length ? snapshotEnvParts.join(' ') + ' ' : '';
// Use correct command: snapshot list ct/<CT_ID> --repository <full_repo_string>
const command = `timeout 30 proxmox-backup-client snapshot list ct/${ctId} --repository ${repository} 2>&1 || echo "PBS_ERROR"`;
const command = `${snapshotEnvStr}timeout 30 proxmox-backup-client snapshot list ct/${ctId} --repository ${repository} 2>&1 || echo "PBS_ERROR"`;
let output = '';
console.log(`[BackupService] Discovering PBS backups for CT ${ctId} on repository ${repository}`);

View File

@@ -250,9 +250,16 @@ class RestoreService {
const targetFolder = `/var/lib/vz/dump/vzdump-lxc-${ctId}-${snapshotNameForPath}`;
const targetTar = `${targetFolder}.tar`;
// Use PBS_PASSWORD env var and add timeout for long downloads
// Use PBS_PASSWORD env var and add timeout for long downloads; PBS_FINGERPRINT when set for cert validation
const escapedPassword = credential.pbs_password.replace(/'/g, "'\\''");
const restoreCommand = `PBS_PASSWORD='${escapedPassword}' PBS_REPOSITORY='${repository}' timeout 300 proxmox-backup-client restore "${snapshotPath}" root.pxar "${targetFolder}" --repository '${repository}' 2>&1`;
const fingerprint = credential.pbs_fingerprint?.trim() ?? '';
const escapedFingerprint = fingerprint ? fingerprint.replace(/'/g, "'\\''") : '';
const restoreEnvParts = [`PBS_PASSWORD='${escapedPassword}'`, `PBS_REPOSITORY='${repository}'`];
if (escapedFingerprint) {
restoreEnvParts.push(`PBS_FINGERPRINT='${escapedFingerprint}'`);
}
const restoreEnvStr = restoreEnvParts.join(' ');
const restoreCommand = `${restoreEnvStr} timeout 300 proxmox-backup-client restore "${snapshotPath}" root.pxar "${targetFolder}" --repository '${repository}' 2>&1`;
let output = '';
let exitCode = 0;