This commit is contained in:
jarek committed 2026-06-15 14:56:51 +02:00
1 parent d465ecfe96
commit 83c3a5ea09
85 files changed
+10118 -1046

No files matched your search

+13 -2
View File
@@ -3,6 +3,7 @@ import type { RequestHandler } from './$types';
import { getRegistries, createRegistry, setDefaultRegistry } from '$lib/server/db';
import { authorize } from '$lib/server/authorize';
import { auditRegistry } from '$lib/server/audit';
import { parseRegistryUrl, DOCKER_HUB_HOSTS } from '$lib/server/docker';
export const GET: RequestHandler = async ({ cookies }) => {
const auth = await authorize(cookies);
@@ -38,11 +39,21 @@ export const POST: RequestHandler = async (event) => {
return json({ error: 'Name and URL are required' }, { status: 400 });
}
// Trim username/password to prevent stray whitespace from copy-paste corrupting
// the X-Registry-Auth / Authorization headers (#1105).
const trimmedUsername = typeof data.username === 'string' ? data.username.trim() : undefined;
const trimmedPassword = typeof data.password === 'string' ? data.password.trim() : undefined;
// Diagnostic logging (#1105) — never logs the plaintext credential
const { host: normalizedHost } = parseRegistryUrl(data.url);
const hubTag = DOCKER_HUB_HOSTS.has(normalizedHost) ? ' (docker-hub)' : '';
console.log(`[Registry] create: url=${data.url} normalized=${normalizedHost}${hubTag} user(len=${trimmedUsername?.length ?? 0}) pw(len=${trimmedPassword?.length ?? 0})`);
const registry = await createRegistry({
name: data.name,
url: data.url,
username: data.username || undefined,
password: data.password || undefined,
username: trimmedUsername || undefined,
password: trimmedPassword || undefined,
isDefault: data.isDefault || false
});
+14 -2
View File
@@ -4,6 +4,7 @@ import { getRegistry, updateRegistry, deleteRegistry, setDefaultRegistry } from
import { authorize } from '$lib/server/authorize';
import { auditRegistry } from '$lib/server/audit';
import { computeAuditDiff } from '$lib/utils/diff';
import { parseRegistryUrl, DOCKER_HUB_HOSTS } from '$lib/server/docker';
export const GET: RequestHandler = async ({ params, cookies }) => {
const auth = await authorize(cookies);
@@ -51,11 +52,22 @@ export const PUT: RequestHandler = async (event) => {
}
const data = await request.json();
// Trim username/password to prevent stray whitespace from copy-paste corrupting
// the X-Registry-Auth / Authorization headers (#1105).
const trimmedUsername = typeof data.username === 'string' ? data.username.trim() : data.username;
const trimmedPassword = typeof data.password === 'string' ? data.password.trim() : data.password;
// Diagnostic logging (#1105) — never logs the plaintext credential
const userLen = typeof trimmedUsername === 'string' ? trimmedUsername.length : 0;
const pwLen = typeof trimmedPassword === 'string' ? trimmedPassword.length : 0;
const { host: normalizedHost } = parseRegistryUrl(data.url);
const hubTag = DOCKER_HUB_HOSTS.has(normalizedHost) ? ' (docker-hub)' : '';
console.log(`[Registry] update id=${id}: url=${data.url} normalized=${normalizedHost}${hubTag} user(len=${userLen}) pw(len=${pwLen})`);
const registry = await updateRegistry(id, {
name: data.name,
url: data.url,
username: data.username,
password: data.password,
username: trimmedUsername,
password: trimmedPassword,
isDefault: data.isDefault
});
+130
View File
@@ -0,0 +1,130 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { authorize } from '$lib/server/authorize';
import { parseRegistryUrl, getRegistryAuthHeader, DOCKER_HUB_HOSTS } from '$lib/server/docker';
import { getRegistry } from '$lib/server/db';
/**
* Test registry connectivity and credentials.
*
* Accepts either inline credentials (from the modal form) or a registry ID
* (to test an already-saved registry using stored credentials).
*/
export const POST: RequestHandler = async ({ request, cookies }) => {
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('registries', 'view')) {
return json({ error: 'Permission denied' }, { status: 403 });
}
const data = await request.json();
let url: string;
let username: string | undefined;
let password: string | undefined;
if (data.registryId) {
// Test a saved registry — fetch credentials from DB
const reg = await getRegistry(data.registryId);
if (!reg) return json({ error: 'Registry not found' }, { status: 404 });
url = reg.url;
username = reg.username || undefined;
password = reg.password || undefined;
} else {
url = data.url;
username = data.username?.trim() || undefined;
password = data.password?.trim() || undefined;
}
if (!url) {
return json({ error: 'URL is required' }, { status: 400 });
}
const parsed = parseRegistryUrl(url);
const apiBase = `${parsed.protocol}://${parsed.host}`;
try {
// Step 1: connectivity — can we reach /v2/ at all?
const pingResp = await fetch(`${apiBase}/v2/`, {
method: 'GET',
headers: { 'User-Agent': 'Dockhand/1.0' },
signal: AbortSignal.timeout(10_000)
});
const hasAuth = pingResp.status === 401;
const isOpen = pingResp.ok;
if (!isOpen && !hasAuth) {
return json({
success: false,
connectivity: false,
message: `Registry returned HTTP ${pingResp.status}`
});
}
// Step 2: if credentials provided, test authentication.
// Empty scope — we only care whether the registry accepts the login.
// Asking for a privileged scope like registry:catalog:* causes Docker
// Hub to reject the request with HTTP 400 for non-admin users even
// when their credentials are valid.
if (username && password) {
const authHeader = await getRegistryAuthHeader(url, '', { username, password });
if (!authHeader) {
return json({
success: false,
connectivity: true,
authenticated: false,
message: 'Authentication failed — check username and password'
});
}
// Verify the token works
const authResp = await fetch(`${apiBase}/v2/`, {
method: 'GET',
headers: {
'User-Agent': 'Dockhand/1.0',
'Authorization': authHeader
},
signal: AbortSignal.timeout(10_000)
});
if (!authResp.ok) {
return json({
success: false,
connectivity: true,
authenticated: false,
message: `Authentication token rejected (HTTP ${authResp.status})`
});
}
const isHub = DOCKER_HUB_HOSTS.has(parsed.host);
return json({
success: true,
connectivity: true,
authenticated: true,
message: isHub
? `Connected to Docker Hub as ${username}`
: `Connected and authenticated as ${username}`
});
}
// No credentials — just report connectivity
const isHub = DOCKER_HUB_HOSTS.has(parsed.host);
return json({
success: true,
connectivity: true,
authenticated: null,
message: isHub
? 'Docker Hub is reachable (no credentials to test)'
: isOpen
? 'Registry is reachable (no auth required)'
: 'Registry is reachable (requires authentication)'
});
} catch (e: any) {
const msg = e?.cause?.code || e?.message || String(e);
return json({
success: false,
connectivity: false,
message: `Connection failed: ${msg}`
});
}
};