fix: node names not DIDs, file sharing path validation, sync results
- nodeName() shows friendly "Node-XXXX" instead of truncated DID - nodeNameFromDid() for sync results lookup - Map labels use node names - Content filename validation: allow / for subdirectories (Music/song.mp3) but still block .., \, null bytes, hidden files, absolute paths - Increased filename max length to 512 for paths with subdirectories Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,15 +35,21 @@ impl RpcHandler {
|
||||
.get("filename")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing filename"))?;
|
||||
// Validate filename: prevent path traversal, hidden files, and excessive length
|
||||
if filename.contains("..") || filename.contains('\0') || filename.contains('/') || filename.contains('\\') {
|
||||
// Validate filename: prevent path traversal and null bytes
|
||||
// Allow forward slashes for subdirectories (e.g., "Music/song.mp3")
|
||||
if filename.contains("..") || filename.contains('\0') || filename.contains('\\') {
|
||||
anyhow::bail!("Invalid filename: path traversal not allowed");
|
||||
}
|
||||
if filename.starts_with('.') {
|
||||
anyhow::bail!("Invalid filename: hidden files not allowed");
|
||||
// Reject paths starting with / (absolute) or . (hidden)
|
||||
if filename.starts_with('/') || filename.starts_with('.') {
|
||||
anyhow::bail!("Invalid filename: absolute paths and hidden files not allowed");
|
||||
}
|
||||
if filename.is_empty() || filename.len() > 255 {
|
||||
anyhow::bail!("Invalid filename: must be 1-255 characters");
|
||||
// Reject any path segment starting with . (hidden dirs)
|
||||
if filename.split('/').any(|seg| seg.starts_with('.') || seg.is_empty()) {
|
||||
anyhow::bail!("Invalid filename: hidden files/dirs or empty segments not allowed");
|
||||
}
|
||||
if filename.is_empty() || filename.len() > 512 {
|
||||
anyhow::bail!("Invalid filename: must be 1-512 characters");
|
||||
}
|
||||
let mime_type = params
|
||||
.get("mime_type")
|
||||
|
||||
@@ -188,7 +188,7 @@
|
||||
<div class="space-y-2">
|
||||
<div v-for="r in syncResults" :key="r.did" class="flex items-center gap-3 p-3 bg-white/5 rounded-lg">
|
||||
<div class="w-2 h-2 rounded-full shrink-0" :class="r.status === 'ok' ? 'bg-green-400' : 'bg-red-400'"></div>
|
||||
<span class="text-sm text-white/80 font-mono truncate">{{ shortDid(r.did) }}</span>
|
||||
<span class="text-sm text-white/80 truncate" :title="r.did">{{ nodeNameFromDid(r.did) }}</span>
|
||||
<span v-if="r.status === 'ok'" class="text-xs text-green-400">{{ r.apps }} apps</span>
|
||||
<span v-else class="text-xs text-red-400 truncate">{{ r.error }}</span>
|
||||
</div>
|
||||
@@ -230,7 +230,7 @@
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<div class="w-2.5 h-2.5 rounded-full shrink-0" :class="isOnline(node) ? 'bg-green-400' : 'bg-white/30'"></div>
|
||||
<span class="text-sm font-medium text-white truncate" :title="node.did">{{ node.name || shortDid(node.did) }}</span>
|
||||
<span class="text-sm font-medium text-white truncate" :title="node.did">{{ nodeName(node) }}</span>
|
||||
<span
|
||||
class="text-xs shrink-0"
|
||||
:class="nodeTransportIcon(node.did).color"
|
||||
@@ -292,7 +292,7 @@
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<div class="w-2.5 h-2.5 rounded-full shrink-0" :class="isOnline(node) ? 'bg-green-400' : 'bg-white/30'"></div>
|
||||
<span class="text-sm font-medium text-white truncate" :title="node.did">{{ node.name || shortDid(node.did) }}</span>
|
||||
<span class="text-sm font-medium text-white truncate" :title="node.did">{{ nodeName(node) }}</span>
|
||||
</div>
|
||||
<span class="text-xs px-2 py-0.5 rounded-full shrink-0" :class="trustBadgeClass(node.trust_level)">{{ node.trust_level }}</span>
|
||||
</div>
|
||||
@@ -578,7 +578,7 @@ const mapNodes = computed(() => {
|
||||
for (const node of nodes.value) {
|
||||
result.push({
|
||||
did: node.did,
|
||||
label: node.name || shortDid(node.did),
|
||||
label: nodeName(node),
|
||||
trust_level: node.trust_level as 'trusted' | 'observer' | 'untrusted',
|
||||
online: isOnline(node),
|
||||
app_count: node.last_state?.apps?.length ?? 0,
|
||||
@@ -773,6 +773,21 @@ function shortDid(did: string): string {
|
||||
return did.slice(0, 16) + '...' + did.slice(-8)
|
||||
}
|
||||
|
||||
/** User-friendly node display name. Prefers name, falls back to "Node-XXXX" from DID hash. */
|
||||
function nodeName(node: { name?: string | null; did: string }): string {
|
||||
if (node.name) return node.name
|
||||
const suffix = node.did.replace(/^did:key:z6Mk/, '').slice(-6).toUpperCase()
|
||||
return `Node-${suffix}`
|
||||
}
|
||||
|
||||
/** Look up display name from DID (for sync results that only have a DID). */
|
||||
function nodeNameFromDid(did: string): string {
|
||||
const node = nodes.value.find(n => n.did === did)
|
||||
if (node) return nodeName(node)
|
||||
const suffix = did.replace(/^did:key:z6Mk/, '').slice(-6).toUpperCase()
|
||||
return `Node-${suffix}`
|
||||
}
|
||||
|
||||
function timeAgo(iso: string): string {
|
||||
const seconds = Math.floor((Date.now() - new Date(iso).getTime()) / 1000)
|
||||
if (seconds < 60) return 'just now'
|
||||
|
||||
Reference in New Issue
Block a user