files: PDFs, text, code, tables and certificates open in place

The file pane decided what to show by MIME type, and uploads keep a
type only when it cannot execute, so a .md, a .json or a .pem arrived
as application/octet-stream and got "no preview". It now goes by name
first (preview.ts), and a file with a name it does not know has its
first 8 KiB looked at: no NUL bytes and valid UTF-8 means text.

- text and code: the first 256 KiB with line numbers, as before
- markdown: rendered through the same sanitiser as documents, with a
  source toggle
- CSV/TSV: a table of the first 500 rows, quotes respected
- PDF: the browser's own viewer in a frame. Only a PDF is served
  without the sandbox CSP (Chrome's viewer will not start under it),
  with frame-ancestors 'self' and X-Frame-Options SAMEORIGIN; the app
  CSP allows same-origin frames.
- certificates, keys, keystores: POST /api/nodes/{id}/inspect reads
  PEM (chains, keys, requests, public keys), DER and PKCS#12 on the
  server, in pure Rust, including the RC2/3DES keystores that
  openssl -legacy and older keychains write. It answers with subject,
  issuer, validity, key, names, usage and fingerprints, and pairs each
  private key with its certificate by public key. A password is asked
  for only when needed, used for that one answer and never stored;
  no key material is ever sent back. Sealed files are read through the
  vault, and only while it is open.

The tests read throwaway fixtures under src/routes/testdata, made with
the openssl commands listed beside them; .gitignore lets their .pem
files past a global *.pem rule.
This commit is contained in:
nvrl committed 2026-10-20 09:57:09 +02:00
1 parent 9749c500cf
commit 1ae0665206
21 files changed
+1571 -22

No files matched your search

+40
View File
@@ -141,6 +141,42 @@ export interface MediaFile {
file_node: number | null
}
/** One thing a certificate, key or keystore file holds, as `/inspect` describes it. */
export type CertItem =
| {
kind: 'certificate'
subject: string
issuer: string
serial: string
not_before: number
not_after: number
/** `RSA 2048`, `EC P-256`, `Ed25519`. */
key: string
names: string[]
usage: string[]
ca: boolean
sha256: string
sha1: string
label: string | null
}
| { kind: 'request'; subject: string; key: string; names: string[] }
| {
kind: 'private_key'
/** Null while it is still encrypted. */
key: string | null
encrypted: boolean
/** The index of its certificate among the items. */
matches: number | null
label: string | null
}
| { kind: 'public_key'; key: string }
export interface Inspection {
items: CertItem[]
/** Something in the file is behind a password that was not given. */
needs_password: boolean
}
/** The vault, as `/api/vault` reports it. */
export interface VaultStatus {
configured: boolean
@@ -403,6 +439,10 @@ export const api = {
method: 'POST',
}),
/** What a certificate, key or keystore holds; the password opens it for this answer only. */
inspect: (id: number, password?: string) =>
request<Inspection>(`/nodes/${id}/inspect`, { method: 'POST', body: body({ password }) }),
backupStatus: () => request<BackupStatus>('/backup'),
backupNow: () => request<BackupStatus>('/backup', { method: 'POST' }),