feat: add Timestamp PDF tool with RFC 3161 support
Add document timestamping to the Secure PDF section using RFC 3161 protocol. Users can select from preset TSA servers (DigiCert, Sectigo, SSL.com, Entrust, FreeTSA) or enter a custom TSA URL. No personal certificate is required — only a cryptographic hash is sent to the server. Key changes: - Timestamp PDF page with TSA server selector, FAQ and SEO structured data - timestampPdf() function with CORS proxy URL resolution - TimestampNode for the workflow engine - Tool entry in Secure PDF category + homepage i18n - Built-in CORS proxy middleware for dev/preview - Translations for all 16 languages Tested with DigiCert, Sectigo and Entrust TSA servers. Timestamps are verifiable in Adobe Acrobat (ETSI.RFC3161 SubFilter).
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { defineConfig, Plugin } from 'vitest/config';
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import type { Connect } from 'vite';
|
||||
import basicSsl from '@vitejs/plugin-basic-ssl';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
@@ -197,13 +199,89 @@ function createLanguageMiddleware(isDev: boolean): Connect.NextHandleFunction {
|
||||
};
|
||||
}
|
||||
|
||||
function createCorsProxyMiddleware(): Connect.NextHandleFunction {
|
||||
return (
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
next: Connect.NextFunction
|
||||
): void => {
|
||||
if (!req.url?.startsWith('/cors-proxy')) return next();
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
res.statusCode = 204;
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = new URL(req.url, 'http://localhost');
|
||||
const targetUrl = parsed.searchParams.get('url');
|
||||
if (!targetUrl) {
|
||||
res.statusCode = 400;
|
||||
res.end('Missing url parameter');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[CORS Proxy] ${req.method} ${targetUrl}`);
|
||||
|
||||
const bodyChunks: Buffer[] = [];
|
||||
req.on('data', (chunk: Buffer) => bodyChunks.push(chunk));
|
||||
req.on('end', () => {
|
||||
const body = Buffer.concat(bodyChunks);
|
||||
const target = new URL(targetUrl);
|
||||
const transport = target.protocol === 'https:' ? https : http;
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
if (req.headers['content-type']) {
|
||||
headers['Content-Type'] = req.headers['content-type'] as string;
|
||||
}
|
||||
if (body.length > 0) {
|
||||
headers['Content-Length'] = String(body.length);
|
||||
}
|
||||
|
||||
const proxyReq = transport.request(
|
||||
targetUrl,
|
||||
{ method: req.method || 'GET', headers },
|
||||
(proxyRes) => {
|
||||
console.log(
|
||||
`[CORS Proxy] Response: ${proxyRes.statusCode} from ${targetUrl}`
|
||||
);
|
||||
res.setHeader(
|
||||
'Access-Control-Allow-Origin',
|
||||
req.headers.origin || '*'
|
||||
);
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
res.statusCode = proxyRes.statusCode || 200;
|
||||
proxyRes.pipe(res);
|
||||
}
|
||||
);
|
||||
|
||||
proxyReq.on('error', (err) => {
|
||||
console.error('[CORS Proxy] Error:', err.message);
|
||||
res.statusCode = 502;
|
||||
res.end(`Proxy error: ${err.message}`);
|
||||
});
|
||||
|
||||
if (body.length > 0) {
|
||||
proxyReq.write(body);
|
||||
}
|
||||
proxyReq.end();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function languageRouterPlugin(): Plugin {
|
||||
return {
|
||||
name: 'language-router',
|
||||
configureServer(server) {
|
||||
server.middlewares.use(createCorsProxyMiddleware());
|
||||
server.middlewares.use(createLanguageMiddleware(true));
|
||||
},
|
||||
configurePreviewServer(server) {
|
||||
server.middlewares.use(createCorsProxyMiddleware());
|
||||
server.middlewares.use(createLanguageMiddleware(false));
|
||||
},
|
||||
};
|
||||
@@ -309,7 +387,7 @@ export default defineConfig(() => {
|
||||
include: ['buffer', 'stream', 'util', 'zlib', 'process'],
|
||||
globals: {
|
||||
Buffer: true,
|
||||
global: true,
|
||||
global: false,
|
||||
process: true,
|
||||
},
|
||||
}),
|
||||
@@ -551,6 +629,7 @@ export default defineConfig(() => {
|
||||
__dirname,
|
||||
'src/pages/digital-sign-pdf.html'
|
||||
),
|
||||
'timestamp-pdf': resolve(__dirname, 'src/pages/timestamp-pdf.html'),
|
||||
'validate-signature-pdf': resolve(
|
||||
__dirname,
|
||||
'src/pages/validate-signature-pdf.html'
|
||||
|
||||
Reference in New Issue
Block a user