← back to Dw Domain Viewer
initial scaffold — DW domain viewer (aa-viewer pattern, 180 CF-sourced subdomains)
c4fe1700acc3ef5df33d914312e8e85888a4dd5c · 2026-07-23 08:06:10 -0700 · Steve Abrams
Files touched
A .gitignoreA domains.jsonA public/index.htmlA scripts/refresh-domains.shA server.js
Diff
commit c4fe1700acc3ef5df33d914312e8e85888a4dd5c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 23 08:06:10 2026 -0700
initial scaffold — DW domain viewer (aa-viewer pattern, 180 CF-sourced subdomains)
---
.gitignore | 8 ++
domains.json | 182 +++++++++++++++++++++++++++++++++++++++++++++
public/index.html | 136 +++++++++++++++++++++++++++++++++
scripts/refresh-domains.sh | 21 ++++++
server.js | 155 ++++++++++++++++++++++++++++++++++++++
5 files changed, 502 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1924158
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
diff --git a/domains.json b/domains.json
new file mode 100644
index 0000000..e36d448
--- /dev/null
+++ b/domains.json
@@ -0,0 +1,182 @@
+[
+ "designerwallcoverings.com",
+ "1838-wallcoverings.designerwallcoverings.com",
+ "all.designerwallcoverings.com",
+ "andrew-martin.designerwallcoverings.com",
+ "anna-french.designerwallcoverings.com",
+ "api.designerwallcoverings.com",
+ "ardsley-textiles.designerwallcoverings.com",
+ "arte-international.designerwallcoverings.com",
+ "artmura.designerwallcoverings.com",
+ "as-creation.designerwallcoverings.com",
+ "astek.designerwallcoverings.com",
+ "atomic-50-ceilings.designerwallcoverings.com",
+ "atomic50.designerwallcoverings.com",
+ "baker-lifestyle.designerwallcoverings.com",
+ "black-edition.designerwallcoverings.com",
+ "brand-mckenzie.designerwallcoverings.com",
+ "british-walls.designerwallcoverings.com",
+ "britishwalls.designerwallcoverings.com",
+ "brunschwig-fils.designerwallcoverings.com",
+ "carlise-co.designerwallcoverings.com",
+ "carlisle-co.designerwallcoverings.com",
+ "carlrobinson.designerwallcoverings.com",
+ "catchii.designerwallcoverings.com",
+ "celebrity.designerwallcoverings.com",
+ "chat.designerwallcoverings.com",
+ "china-seas.designerwallcoverings.com",
+ "china.designerwallcoverings.com",
+ "clarke-and-clarke.designerwallcoverings.com",
+ "cmo-paris.designerwallcoverings.com",
+ "coastline-reserve.designerwallcoverings.com",
+ "cole-son-sg.designerwallcoverings.com",
+ "cole-son.designerwallcoverings.com",
+ "coleandson.designerwallcoverings.com",
+ "colefax-fowler.designerwallcoverings.com",
+ "colour-design.designerwallcoverings.com",
+ "coordonn.designerwallcoverings.com",
+ "cowtan-tout.designerwallcoverings.com",
+ "cowtan.designerwallcoverings.com",
+ "crezana-internal.designerwallcoverings.com",
+ "crezana.designerwallcoverings.com",
+ "daisybennett.designerwallcoverings.com",
+ "dedar.designerwallcoverings.com",
+ "denovo-wall.designerwallcoverings.com",
+ "designers-guild.designerwallcoverings.com",
+ "designtex.designerwallcoverings.com",
+ "dolce-gabbana.designerwallcoverings.com",
+ "donghia.designerwallcoverings.com",
+ "eijffinger.designerwallcoverings.com",
+ "elitis.designerwallcoverings.com",
+ "fabricut.designerwallcoverings.com",
+ "fallingstar.designerwallcoverings.com",
+ "farrow-ball.designerwallcoverings.com",
+ "fentucci.designerwallcoverings.com",
+ "fernwood-weave.designerwallcoverings.com",
+ "followup.designerwallcoverings.com",
+ "franquemont.designerwallcoverings.com",
+ "fromental-internal.designerwallcoverings.com",
+ "fromental.designerwallcoverings.com",
+ "gaston-y-daniela.designerwallcoverings.com",
+ "glitter-wallcoverings.designerwallcoverings.com",
+ "glitter-walls.designerwallcoverings.com",
+ "goodrich.designerwallcoverings.com",
+ "gp-j-baker.designerwallcoverings.com",
+ "gracie-internal.designerwallcoverings.com",
+ "gracie.designerwallcoverings.com",
+ "graham-brown.designerwallcoverings.com",
+ "greenland.designerwallcoverings.com",
+ "harlequin-sg.designerwallcoverings.com",
+ "harlequin.designerwallcoverings.com",
+ "holly-hunt.designerwallcoverings.com",
+ "hollywood-wallcoverings.designerwallcoverings.com",
+ "hygge-west.designerwallcoverings.com",
+ "innovations.designerwallcoverings.com",
+ "internal.astek.designerwallcoverings.com",
+ "internal.crezana.designerwallcoverings.com",
+ "internal.fromental.designerwallcoverings.com",
+ "internal.gracie.designerwallcoverings.com",
+ "internal.greenland.designerwallcoverings.com",
+ "internal.momentum.designerwallcoverings.com",
+ "internal.reidwitlin.designerwallcoverings.com",
+ "internal.schumacher.designerwallcoverings.com",
+ "internal.zuber.designerwallcoverings.com",
+ "j-josephson.designerwallcoverings.com",
+ "japan.designerwallcoverings.com",
+ "jeffrey-stevens.designerwallcoverings.com",
+ "jf-fabrics.designerwallcoverings.com",
+ "jim-dultz.designerwallcoverings.com",
+ "kamism.designerwallcoverings.com",
+ "kirkby-design.designerwallcoverings.com",
+ "knoll.designerwallcoverings.com",
+ "koroseal-sg.designerwallcoverings.com",
+ "koroseal.designerwallcoverings.com",
+ "kravet.designerwallcoverings.com",
+ "la-scala-milano.designerwallcoverings.com",
+ "lawalls.designerwallcoverings.com",
+ "lee-jofa.designerwallcoverings.com",
+ "leed-walls.designerwallcoverings.com",
+ "leedwalls.designerwallcoverings.com",
+ "lillianaugust.designerwallcoverings.com",
+ "lincrusta.designerwallcoverings.com",
+ "maharam.designerwallcoverings.com",
+ "malibu-wallpaper.designerwallcoverings.com",
+ "manuel-canovas.designerwallcoverings.com",
+ "marburg.designerwallcoverings.com",
+ "mark-alexander.designerwallcoverings.com",
+ "marketing.designerwallcoverings.com",
+ "maya-romanoff.designerwallcoverings.com",
+ "milton-king.designerwallcoverings.com",
+ "mind-the-gap.designerwallcoverings.com",
+ "momentum.designerwallcoverings.com",
+ "mulberry.designerwallcoverings.com",
+ "murals.designerwallcoverings.com",
+ "naturale-54.designerwallcoverings.com",
+ "new.designerwallcoverings.com",
+ "newmor.designerwallcoverings.com",
+ "newwall.designerwallcoverings.com",
+ "nina-campbell.designerwallcoverings.com",
+ "nlxl.designerwallcoverings.com",
+ "nobilis.designerwallcoverings.com",
+ "novasuede.designerwallcoverings.com",
+ "omexco.designerwallcoverings.com",
+ "p3tec.designerwallcoverings.com",
+ "pairs.designerwallcoverings.com",
+ "paperandink.designerwallcoverings.com",
+ "philipjeffries.designerwallcoverings.com",
+ "phillip-jeffries.designerwallcoverings.com",
+ "phillipe-romano-textiles.designerwallcoverings.com",
+ "phillipe-romano-vinyls.designerwallcoverings.com",
+ "phillipe-romano.designerwallcoverings.com",
+ "photo.designerwallcoverings.com",
+ "photocapture.designerwallcoverings.com",
+ "pierre-frey.designerwallcoverings.com",
+ "pitch.designerwallcoverings.com",
+ "pitchguard.designerwallcoverings.com",
+ "prestige.designerwallcoverings.com",
+ "primoleathers.designerwallcoverings.com",
+ "quadrille-showroom.designerwallcoverings.com",
+ "quadrille.designerwallcoverings.com",
+ "quadrilleshowroom.designerwallcoverings.com",
+ "rebel-walls.designerwallcoverings.com",
+ "reid-witlin.designerwallcoverings.com",
+ "reidwitlin.designerwallcoverings.com",
+ "roberto-cavalli.designerwallcoverings.com",
+ "romo.designerwallcoverings.com",
+ "sancar.designerwallcoverings.com",
+ "sandberg.designerwallcoverings.com",
+ "sangetsu.designerwallcoverings.com",
+ "scalamandre.designerwallcoverings.com",
+ "schumacher.designerwallcoverings.com",
+ "sister-parish.designerwallcoverings.com",
+ "staged.designerwallcoverings.com",
+ "studio-printworks.designerwallcoverings.com",
+ "substitutes.designerwallcoverings.com",
+ "surface-stick.designerwallcoverings.com",
+ "surfacestick.designerwallcoverings.com",
+ "tecnografica.designerwallcoverings.com",
+ "texam.designerwallcoverings.com",
+ "texdecor.designerwallcoverings.com",
+ "thibaut.designerwallcoverings.com",
+ "threads.designerwallcoverings.com",
+ "timorous-beasties.designerwallcoverings.com",
+ "trending.designerwallcoverings.com",
+ "tres-tintas.designerwallcoverings.com",
+ "vahallan.designerwallcoverings.com",
+ "versa-design-surfaces.designerwallcoverings.com",
+ "versace.designerwallcoverings.com",
+ "villa-nova.designerwallcoverings.com",
+ "wallquest.designerwallcoverings.com",
+ "william-morris.designerwallcoverings.com",
+ "wolf-gordon.designerwallcoverings.com",
+ "york-contract.designerwallcoverings.com",
+ "york-wallcoverings.designerwallcoverings.com",
+ "zambaiti-parati.designerwallcoverings.com",
+ "zambaiti.designerwallcoverings.com",
+ "zimmer-rohde.designerwallcoverings.com",
+ "zinc-textile.designerwallcoverings.com",
+ "zintra.designerwallcoverings.com",
+ "zoffany.designerwallcoverings.com",
+ "zuber-internal.designerwallcoverings.com",
+ "zuber.designerwallcoverings.com"
+]
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..7724fc2
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,136 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>designerwallcoverings.com — Domain Viewer</title>
+<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%231e2a3a'/%3E%3Ctext x='8' y='12' text-anchor='middle' font-family='Georgia,serif' font-size='11' fill='%23f7f1e6'%3Ed%3C/text%3E%3C/svg%3E">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<style>
+ :root { --rail: 320px; --bg:#0e1116; --panel:#161b22; --line:#2b313b; --txt:#dfe6ee; --dim:#8b95a3; --accent:#3fb6a8; }
+ * { box-sizing: border-box; }
+ body { margin:0; height:100vh; display:flex; font:14px/1.45 -apple-system, "SF Pro Text", Helvetica, sans-serif; background:var(--bg); color:var(--txt); }
+
+ #rail { width:var(--rail); min-width:240px; max-width:500px; display:flex; flex-direction:column; border-right:1px solid var(--line); background:var(--panel); }
+ #rail header { padding:14px 14px 10px; border-bottom:1px solid var(--line); }
+ #rail h1 { margin:0 0 8px; font-size:15px; font-weight:700; letter-spacing:.02em; }
+ #rail h1 small { color:var(--dim); font-weight:400; }
+ #q { width:100%; padding:7px 10px; border-radius:7px; border:1px solid var(--line); background:var(--bg); color:var(--txt); outline:none; }
+ #q:focus { border-color:var(--accent); }
+ #list { flex:1; overflow-y:auto; padding:6px; }
+ .item { display:flex; align-items:center; gap:8px; padding:6px 9px; border-radius:7px; cursor:pointer; color:var(--txt); user-select:none; }
+ .item:hover { background:#1e2530; }
+ .item.active { background:#213132; outline:1px solid var(--accent); }
+ .dot { width:8px; height:8px; border-radius:50%; flex:none; background:#555; }
+ .dot.up { background:#3fbf6f; }
+ .dot.auth { background:#d9a53a; }
+ .dot.down { background:#d95555; }
+ .name { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
+ .name b { color:var(--accent); font-weight:600; }
+ .tag { flex:none; font-size:9.5px; letter-spacing:.04em; color:var(--dim); border:1px solid var(--line); border-radius:4px; padding:1px 4px; }
+ .ms { color:var(--dim); font-size:11px; flex:none; }
+ #foot { padding:8px 14px; border-top:1px solid var(--line); color:var(--dim); font-size:11.5px; }
+
+ #main { flex:1; display:flex; flex-direction:column; min-width:0; }
+ #bar { display:flex; align-items:center; gap:10px; padding:8px 12px; border-bottom:1px solid var(--line); background:var(--panel); }
+ #cur { font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
+ #cur a { color:var(--accent); text-decoration:none; }
+ #bar .sp { flex:1; }
+ button { padding:6px 12px; border-radius:7px; border:1px solid var(--line); background:var(--bg); color:var(--txt); cursor:pointer; font-size:13px; }
+ button:hover { border-color:var(--accent); color:var(--accent); }
+ #frame { flex:1; border:0; width:100%; background:#fff; }
+ #empty { flex:1; display:flex; align-items:center; justify-content:center; color:var(--dim); flex-direction:column; gap:8px; }
+ #empty .big { font-size:40px; }
+</style>
+</head>
+<body>
+ <div id="rail">
+ <header>
+ <h1>designerwallcoverings.com <small id="count"></small></h1>
+ <input id="q" type="search" placeholder="Filter sites…" autocomplete="off">
+ </header>
+ <div id="list"></div>
+ <div id="foot">click a site → renders in the right panel (unified admin pw auto-applied) · dot: <span style="color:#3fbf6f">up</span> / <span style="color:#d9a53a">auth-gated</span> / <span style="color:#d95555">down or pw rejected</span></div>
+ </div>
+ <div id="main">
+ <div id="bar">
+ <span id="cur">— pick a site</span>
+ <span class="sp"></span>
+ <button id="reload" title="Reload iframe">⟳ Reload</button>
+ <button id="newtab" title="Open in a new browser tab">↗ New Tab</button>
+ </div>
+ <div id="empty"><div class="big">🌐</div><div>Select a site on the left to view it live here.</div></div>
+ <iframe id="frame" style="display:none"></iframe>
+ </div>
+
+<script>
+let domains = [], status = {}, current = localStorage.getItem('dwv-current') || '';
+const list = document.getElementById('list'), q = document.getElementById('q');
+const frame = document.getElementById('frame'), empty = document.getElementById('empty');
+const cur = document.getElementById('cur'), count = document.getElementById('count');
+
+// internal.* and *-internal hosts are curation tools, not customer-facing microsites.
+const isInternal = (d) => /^internal\./.test(d) || /-internal\./.test(d);
+// Rail shows the short label; the full host lives in the title tooltip.
+const shortName = (d) => d === 'designerwallcoverings.com' ? '● apex (DW store)' : d.replace('.designerwallcoverings.com', '');
+
+function render() {
+ const f = q.value.trim().toLowerCase();
+ const shown = domains.filter(d => d.includes(f));
+ count.textContent = `(${shown.length}/${domains.length})`;
+ list.innerHTML = '';
+ for (const d of shown) {
+ const s = status[d];
+ const row = document.createElement('div');
+ row.className = 'item' + (d === current ? ' active' : '');
+ row.title = d;
+ const dotCls = !s ? '' : s.authFail ? 'down' : s.gated ? 'auth' : s.up ? 'up' : 'down';
+ const label = f ? shortName(d).replace(f, `<b>${f}</b>`) : shortName(d);
+ row.innerHTML = `<span class="dot ${dotCls}"></span><span class="name">${label}</span>` +
+ (isInternal(d) ? `<span class="tag">INT</span>` : '') +
+ (s && s.up ? `<span class="ms">${s.ms}ms</span>` : s ? `<span class="ms">—</span>` : '');
+ row.onclick = () => select(d);
+ list.appendChild(row);
+ }
+}
+
+function select(d) {
+ current = d;
+ localStorage.setItem('dwv-current', d);
+ cur.innerHTML = `<a href="https://${d}" target="_blank" rel="noopener noreferrer">${d}</a>`;
+ // Load through the server-side proxy — it injects the unified admin credential
+ // and strips X-Frame-Options, so every site renders in-panel without prompting.
+ empty.style.display = 'none';
+ frame.style.display = 'block';
+ frame.src = '/site/' + d + '/';
+ render();
+}
+
+document.getElementById('reload').onclick = () => { if (current) frame.src = '/site/' + current + '/'; };
+document.getElementById('newtab').onclick = () => { if (current) window.open('https://' + current, '_blank', 'noopener'); };
+q.oninput = render;
+
+(async () => {
+ try {
+ const dres = await fetch('/api/domains');
+ if (!dres.ok) throw new Error('domains HTTP ' + dres.status);
+ domains = await dres.json();
+ render();
+ if (current && domains.includes(current)) select(current);
+ } catch (e) {
+ count.textContent = '(load failed — reload)';
+ return;
+ }
+ // Health dots load after the list — probes take ~60-90s for 180 domains. A fetch
+ // aborted mid-flight (tab closed, server restarted) must not surface as an
+ // unhandled rejection; the list already rendered, dots are best-effort.
+ try {
+ const sres = await fetch('/api/status');
+ if (!sres.ok) throw new Error('status HTTP ' + sres.status);
+ const st = await sres.json();
+ for (const s of st) status[s.domain] = s;
+ render();
+ } catch (e) { /* dots stay grey; next reload retries */ }
+})();
+</script>
+</body>
+</html>
diff --git a/scripts/refresh-domains.sh b/scripts/refresh-domains.sh
new file mode 100755
index 0000000..85f1ef1
--- /dev/null
+++ b/scripts/refresh-domains.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+# Re-pull the *.designerwallcoverings.com record list from Cloudflare into domains.json.
+# Run after adding/removing a subdomain in the CF zone. $0 (local + CF API reads).
+set -euo pipefail
+export $(grep -E "^CLOUDFLARE_API_TOKEN" ~/Projects/secrets-manager/.env | xargs)
+ZONE=$(curl -s "https://api.cloudflare.com/client/v4/zones?name=designerwallcoverings.com" \
+ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" | python3 -c "import json,sys; print(json.load(sys.stdin)['result'][0]['id'])")
+curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE/dns_records?per_page=200" \
+ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" | python3 -c "
+import json, sys, os
+d = json.load(sys.stdin)
+names = sorted(set(r['name'] for r in d['result'] if r['type'] in ('A', 'AAAA', 'CNAME')))
+drop = {'_domainconnect.designerwallcoverings.com',
+ 'workforce.agentabrams.com.designerwallcoverings.com',
+ 'www.designerwallcoverings.com'}
+names = [n for n in names if n not in drop]
+names.sort(key=lambda n: (n != 'designerwallcoverings.com', n))
+out = os.path.expanduser('~/Projects/dw-domain-viewer/domains.json')
+json.dump(names, open(out, 'w'), indent=1)
+print(len(names), 'domains written to', out)
+"
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..49e0353
--- /dev/null
+++ b/server.js
@@ -0,0 +1,155 @@
+#!/usr/bin/env node
+// dw-domain-viewer — left-panel list of every *.designerwallcoverings.com site,
+// right-panel view of the live site. Pattern cloned from agentabrams-viewer (:9783).
+// Zero-dependency Node http server. Basic auth admin/DW2024! (internal-viewer standard).
+// The right panel loads /site/<domain>/… — a reverse proxy that injects the unified
+// admin credential upstream (browsers refuse to basic-auth cross-origin iframes) and
+// strips X-Frame-Options/CSP so every fleet site renders inside the panel.
+const http = require('http');
+const https = require('https');
+const fs = require('fs');
+const path = require('path');
+
+const PORT = process.env.PORT || 9784;
+const ROOT = __dirname;
+const AUTH = 'Basic ' + Buffer.from(process.env.BASIC_AUTH || 'admin:DW2024!').toString('base64');
+// Credential injected into upstream fleet sites (the unified admin pw).
+const UPSTREAM_AUTH = 'Basic ' + Buffer.from(process.env.UPSTREAM_AUTH || 'admin:DW2024!').toString('base64');
+
+const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.png': 'image/png', '.svg': 'image/svg+xml' };
+
+function loadDomains() {
+ return JSON.parse(fs.readFileSync(path.join(ROOT, 'domains.json'), 'utf8'));
+}
+
+// Server-side health probe — the browser can't check cross-origin status itself.
+// Gated sites get a second HEAD with the unified credential so a dead password
+// (authFail) is distinguishable from a healthy auth gate.
+function head(domain, withAuth) {
+ return new Promise((resolve) => {
+ const headers = withAuth ? { authorization: UPSTREAM_AUTH } : {};
+ const req = https.request({ host: domain, method: 'HEAD', path: '/', headers, timeout: 6000, rejectUnauthorized: true }, (res) => {
+ res.resume();
+ resolve(res.statusCode);
+ });
+ req.on('timeout', () => { req.destroy(); resolve(0); });
+ req.on('error', () => resolve(0));
+ req.end();
+ });
+}
+
+async function probe(domain) {
+ const started = Date.now();
+ const plain = await head(domain, false);
+ let gated = plain === 401, authFail = false, effective = plain;
+ if (gated) {
+ effective = await head(domain, true);
+ authFail = effective === 401;
+ }
+ return {
+ domain,
+ status: plain,
+ ms: Date.now() - started,
+ gated,
+ authFail,
+ up: effective !== 0 && effective !== 401 && effective < 500,
+ };
+}
+
+const server = http.createServer(async (req, res) => {
+ const url = new URL(req.url, 'http://x');
+
+ if (url.pathname === '/healthz') { res.writeHead(200); return res.end('ok'); }
+
+ if (req.headers.authorization !== AUTH) {
+ res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="dw-domain-viewer"' });
+ return res.end('auth required');
+ }
+
+ if (url.pathname === '/api/domains') {
+ res.writeHead(200, { 'Content-Type': 'application/json' });
+ return res.end(JSON.stringify(loadDomains()));
+ }
+
+ if (url.pathname === '/api/status') {
+ const domains = loadDomains();
+ // Probe in batches of 16 so 180 HEADs don't stampede.
+ const out = [];
+ for (let i = 0; i < domains.length; i += 16) {
+ out.push(...await Promise.all(domains.slice(i, i + 16).map(probe)));
+ }
+ res.writeHead(200, { 'Content-Type': 'application/json' });
+ return res.end(JSON.stringify(out));
+ }
+
+ // Reverse proxy: /site/<domain>/<path> → https://<domain>/<path> with the
+ // unified admin credential injected. Only domains in domains.json are proxied.
+ if (url.pathname.startsWith('/site/')) {
+ const rest = url.pathname.slice('/site/'.length);
+ const slash = rest.indexOf('/');
+ const domain = slash < 0 ? rest : rest.slice(0, slash);
+ if (slash < 0) { res.writeHead(302, { Location: `/site/${domain}/` }); return res.end(); }
+ const domains = loadDomains();
+ if (!domains.includes(domain)) { res.writeHead(404); return res.end('unknown domain'); }
+ const upstreamPath = rest.slice(slash) + url.search;
+ const headers = { ...req.headers, host: domain, authorization: UPSTREAM_AUTH };
+ delete headers.connection;
+ const preq = https.request({ host: domain, method: req.method, path: upstreamPath, headers, timeout: 20000, rejectUnauthorized: true }, (pres) => {
+ const h = { ...pres.headers };
+ // The whole point of the proxy: let everything render in the panel.
+ delete h['x-frame-options'];
+ delete h['content-security-policy'];
+ delete h['strict-transport-security'];
+ // The root-relative asset escape hatch below routes by Referer; an upstream
+ // no-referrer policy would blind it and every /assets/* request would 404.
+ delete h['referrer-policy'];
+ delete h.connection;
+ if (h.location) {
+ try {
+ const loc = new URL(h.location, `https://${domain}`);
+ if (domains.includes(loc.host)) h.location = `/site/${loc.host}${loc.pathname}${loc.search}`;
+ } catch {}
+ }
+ if (h['set-cookie']) {
+ h['set-cookie'] = h['set-cookie'].map((c) => {
+ // A Domain= attribute means the cookie is cross-subdomain — scope it to
+ // /site/ so it follows the user across every proxied domain.
+ const fleetWide = /;\s*Domain=/i.test(c);
+ return c
+ .replace(/;\s*Domain=[^;]+/gi, '')
+ // Viewer is served over plain http on loopback; Safari drops Secure cookies there.
+ .replace(/;\s*Secure/gi, '')
+ .replace(/;\s*Path=\/([^;]*)/i, fleetWide ? '; Path=/site/' : `; Path=/site/${domain}/$1`);
+ });
+ }
+ res.writeHead(pres.statusCode, h);
+ pres.pipe(res);
+ });
+ preq.on('timeout', () => preq.destroy(new Error('upstream timeout')));
+ preq.on('error', (e) => { if (res.writableEnded) return; if (!res.headersSent) res.writeHead(502); res.end('proxy error: ' + e.message); });
+ req.pipe(preq);
+ return;
+ }
+
+ let file = url.pathname === '/' ? '/index.html' : url.pathname;
+ const fp = path.join(ROOT, 'public', path.normalize(file).replace(/^(\.\.[\/\\])+/, ''));
+ if (!fp.startsWith(path.join(ROOT, 'public'))) { res.writeHead(403); return res.end(); }
+
+ // Root-relative escape hatch: a proxied page requesting "/app.js" lands here.
+ // If the request came from inside /site/<domain>/, bounce it back into the proxy.
+ if (!fs.existsSync(fp)) {
+ const m = (req.headers.referer || '').match(/\/site\/([^/]+)\//);
+ if (m && loadDomains().includes(m[1])) {
+ res.writeHead(302, { Location: `/site/${m[1]}${url.pathname}${url.search}` });
+ return res.end();
+ }
+ }
+
+ fs.readFile(fp, (err, data) => {
+ if (err) { res.writeHead(404); return res.end('not found'); }
+ res.writeHead(200, { 'Content-Type': MIME[path.extname(fp)] || 'application/octet-stream' });
+ res.end(data);
+ });
+});
+
+server.listen(PORT, () => console.log(`dw-domain-viewer on http://127.0.0.1:${PORT}`));
(oldest)
·
back to Dw Domain Viewer
·
fleet green pass: certs+443 for 4 vendor landings, 8 dead DN af1ed5d →