import 'dotenv/config'; import express from 'express'; import fileUpload from 'express-fileupload'; import stream from 'stream'; import { addBytes, getCID } from './services/ipfs'; import App from '../web/pages/App'; import { Provider } from 'react-redux'; import React from 'react'; import { renderToString } from 'react-dom/server'; import { StaticRouter } from 'react-router-dom/server'; import configureAppStore, { AppRootState } from '../web/store'; // @ts-ignore import { verify } from '../rs/verifier/index.node'; // @ts-ignore import { verify as verifyV7 } from '../rs/0.1.0-alpha.7/index.node'; import { Attestation } from '../web/utils/types/types'; import { IncomingMessage } from 'node:http'; import { createServer } from 'http'; import { WebSocketServer, type RawData, type WebSocket } from 'ws'; import crypto from 'crypto'; import qs from 'qs'; import { convertNotaryWsToHttp } from '../utils/url'; const app = express(); const port = process.env.PORT || 3000; const server = createServer(app); const wss = new WebSocketServer({ server }); app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader( 'Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content, Accept, Content-Type, Authorization', ); res.setHeader( 'Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS', ); res.setHeader('Cross-origin-Embedder-Policy', 'require-corp'); res.setHeader('Cross-origin-Opener-Policy', 'same-origin'); if (req.method === 'OPTIONS') { res.sendStatus(200); } else { next(); } }); app.use(express.static('build/ui')); app.use( fileUpload({ limits: { fileSize: 1024 * 1024 }, // 1mb file limit }), ); app.post('/api/upload', async (req, res) => { for (const file of Object.values(req.files!)) { // @ts-ignore const data = file.data; const cid = await addBytes(data); res.json(cid); return; } res.status(400).send({ error: true, message: 'request is missing file' }); }); app.get('/gateway/ipfs/:cid', async (req, res) => { const cid = req.params.cid; const file = await getCID(req.params.cid); const readStream = new stream.PassThrough(); readStream.end(Buffer.from(file)); res.set('Content-Type', 'application/octet-stream'); res.set('Content-Disposition', `attachment; filename=${cid}.json`); readStream.pipe(res); }); app.get('/ipfs/:cid', async (req, res) => { // If there is no file from CID or JSON cannot be parsed, redirect to root try { const { cid } = req.params; const [, isWasm] = cid.split('.'); if (isWasm) { return res.redirect(`/${cid}`); } const storeConfig: AppRootState = { notaryKey: { key: '' }, proofUpload: { proofs: [], selectedProof: null, }, proofs: { ipfs: {} }, }; const file = await getCID(req.params.cid); const jsonProof: Attestation = JSON.parse(file); storeConfig.proofs.ipfs[req.params.cid] = { raw: jsonProof, }; /** * Verify the proof if notary url exist * redirect to root if verification fails */ if (!jsonProof.version && jsonProof.notaryUrl) { const notaryPem = await fetchPublicKeyFromNotary(jsonProof.notaryUrl); const proof = await verify(file, notaryPem); proof.notaryUrl = jsonProof.notaryUrl; storeConfig.proofs.ipfs[req.params.cid].proof = { ...proof, version: '0.1.0-alpha.5', notaryUrl: jsonProof.notaryUrl, notaryKey: notaryPem, }; } else if (jsonProof.version) { const notaryUrl = convertNotaryWsToHttp(jsonProof.meta.notaryUrl); const notaryPem = await fetchPublicKeyFromNotary(notaryUrl).catch( () => '', ); const proof = await verifyV7(jsonProof.data, notaryPem); proof.notaryUrl = jsonProof.meta.notaryUrl; storeConfig.proofs.ipfs[req.params.cid].proof = { version: jsonProof.version, time: proof.time, sent: proof.sent, recv: proof.recv, notaryUrl: notaryUrl, websocketProxyUrl: jsonProof.meta.websocketProxyUrl, notaryKey: Buffer.from( notaryPem .replace('-----BEGIN PUBLIC KEY-----', '') .replace('-----END PUBLIC KEY-----', '') .replace(/\n/g, ''), 'base64', ) .slice(23) .toString('hex'), }; } const store = configureAppStore(storeConfig); const html = renderToString( , ); const preloadedState = store.getState(); const imgUrl = 'data:image/svg+xml,' + encodeURIComponent(` `) .replace(/'/g, '%27') .replace(/"/g, '%22'); res.send(` TLSNotary Explorer
${html}
`); } catch (e) { console.error(e); res.redirect('/'); return; } }); app.get('*', (req, res) => { const storeConfig: AppRootState = { notaryKey: { key: '' }, proofUpload: { proofs: [], selectedProof: null, }, proofs: { ipfs: {} }, }; const store = configureAppStore(storeConfig); const html = renderToString( , ); const preloadedState = store.getState(); res.send(` TLSNotary Explorer
${html}
`); }); server.listen(port, () => { console.log(`explorer server listening on port ${port}`); }); const clients: Map = new Map(); const pairs: Map = new Map(); wss.on('connection', async (client: WebSocket, request: IncomingMessage) => { const query = qs.parse((request.url || '').replace(/\/\?/g, '')); const clientId = (query?.clientId as string) || crypto.randomUUID(); clients.set(clientId, client); console.log(`New Connection - ${clientId}`); if (!clientId.includes(':proof')) { await send( clientId, bufferify({ method: 'client_connect', params: { clientId }, }), ); } // set up client event listeners: client.on('message', onClientMessage); client.on('close', endClient); async function endClient() { clients.delete(clientId); if (!clientId.includes(':proof')) { const pair = pairs.get(clientId); if (pair) { pairs.delete(pair); pairs.delete(clientId); await send( pair, bufferify({ method: 'pair_disconnect', params: { pairId: clientId }, }), ); } } console.log(`Connection closed - ${clientId}`); } async function onClientMessage(rawData: RawData) { try { const msg = safeParseJSON(rawData.toString()); if (!msg) { const [cid] = clientId.split(':'); const pairedClientId = pairs.get(cid); await send(pairedClientId + ':proof', rawData); return; } const { to } = msg.params; switch (msg.method) { case 'pair_request': case 'pair_request_sent': case 'pair_request_cancel': case 'pair_request_cancelled': case 'pair_request_reject': case 'pair_request_rejected': case 'pair_request_accept': case 'request_proof': case 'request_proof_by_hash': case 'request_proof_by_hash_failed': case 'proof_request_received': case 'proof_request_accept': case 'verifier_started': case 'prover_setup': case 'prover_started': case 'proof_request_start': case 'proof_request_cancelled': case 'proof_request_rejected': case 'proof_request_cancel': case 'proof_request_reject': case 'proof_request_end': console.log('method:', msg.method); await send(to, rawData); break; case 'pair_request_success': { console.log('method:', msg.method); if (await send(to, rawData)) { pairs.set(to, clientId); pairs.set(clientId, to); } break; } case 'ping': break; default: console.log('unknown msg', msg); break; } } catch (e) { console.error(e); } } // This function broadcasts messages to all webSocket clients function broadcast(data: string) { clients.forEach((c) => c.send(data)); } async function send(clientId: string, data: RawData) { return new Promise((resolve) => { const target = clients.get(clientId); if (!target) { client.send( bufferify({ error: { message: `client "${clientId}" does not exist`, }, }), (err) => { resolve(false); }, ); } else { target.send(data, (err) => { resolve(!err); }); } }); } }); function bufferify(data: any) { return Buffer.from(JSON.stringify(data)); } async function fetchPublicKeyFromNotary(notaryUrl: string) { const res = await fetch(notaryUrl + '/info'); const json: any = await res.json(); if (!json.publicKey) throw new Error('invalid response'); return json.publicKey; } function safeParseJSON(data: string) { try { return JSON.parse(data); } catch (e) { return null; } }