first commit
This commit is contained in:
parent
2f274df5f8
commit
7f02ef91e4
130
server.js
130
server.js
@ -83,6 +83,8 @@ app.post('/admin/save', ensureAdmin, (req, res) => {
|
||||
pathPrefix: req.body.pathPrefix || '/',
|
||||
upstream: req.body.upstream || ''
|
||||
};
|
||||
// mask upstream option: rewrite absolute URLs and cookies so proxy origin is used
|
||||
hostObj.maskUpstream = req.body.mask_upstream === 'on';
|
||||
if (req.body.oidc_issuer) {
|
||||
hostObj.oidc = {
|
||||
issuer: req.body.oidc_issuer,
|
||||
@ -145,7 +147,7 @@ app.use('/preview-proxy/:id', ensureAdmin, (req, res, next) => {
|
||||
const proxy = createProxyMiddleware({
|
||||
target: host.upstream,
|
||||
changeOrigin: true,
|
||||
selfHandleResponse: false,
|
||||
selfHandleResponse: true,
|
||||
proxyTimeout: 15000,
|
||||
pathRewrite: (path, req2) => {
|
||||
// path starts with /preview-proxy/:id
|
||||
@ -163,14 +165,67 @@ app.use('/preview-proxy/:id', ensureAdmin, (req, res, next) => {
|
||||
if (req.session && req.session.isAdmin) proxyReq.setHeader('X-Admin-User', req.session.username || 'admin');
|
||||
},
|
||||
onProxyRes(proxyRes, req2, res2) {
|
||||
// helper: compute proxy origin used by clients
|
||||
const proto = (req2.headers['x-forwarded-proto'] || req2.protocol || 'http');
|
||||
const proxyOrigin = `${proto}://${req2.get('host')}`;
|
||||
// Remove headers that would prevent embedding in an iframe
|
||||
try {
|
||||
delete proxyRes.headers['x-frame-options'];
|
||||
delete proxyRes.headers['x-content-security-policy'];
|
||||
delete proxyRes.headers['content-security-policy'];
|
||||
delete proxyRes.headers['frame-options'];
|
||||
} catch (e) {
|
||||
// ignore
|
||||
} catch (e) {}
|
||||
|
||||
// If masking is enabled for this host, perform stronger rewrites
|
||||
if (host.maskUpstream) {
|
||||
// rewrite Location header
|
||||
if (proxyRes.headers.location) {
|
||||
try {
|
||||
const loc = proxyRes.headers.location;
|
||||
if (loc.startsWith(host.upstream)) {
|
||||
const newLoc = loc.replace(host.upstream, proxyOrigin + (host.pathPrefix && host.pathPrefix !== '/' ? host.pathPrefix : ''));
|
||||
proxyRes.headers.location = newLoc;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// rewrite Set-Cookie: remove Domain attribute
|
||||
if (proxyRes.headers['set-cookie']) {
|
||||
try {
|
||||
const cookies = proxyRes.headers['set-cookie'].map(cookie => cookie.replace(/;\s*Domain=[^;]+/i, ''));
|
||||
proxyRes.headers['set-cookie'] = cookies;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const contentType = (proxyRes.headers['content-type'] || '').toLowerCase();
|
||||
if (contentType.includes('text/html')) {
|
||||
const chunks = [];
|
||||
proxyRes.on('data', chunk => chunks.push(chunk));
|
||||
proxyRes.on('end', () => {
|
||||
try {
|
||||
const body = Buffer.concat(chunks).toString('utf8');
|
||||
const upstreamOrigin = host.upstream.replace(/\/$/, '');
|
||||
const from = new RegExp(upstreamOrigin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
|
||||
const prefix = (host.pathPrefix && host.pathPrefix !== '/') ? host.pathPrefix.replace(/\/$/, '') : '';
|
||||
const replaced = body.replace(from, proxyOrigin + prefix);
|
||||
Object.keys(proxyRes.headers).forEach(h => {
|
||||
if (h.toLowerCase() === 'content-length') delete proxyRes.headers[h];
|
||||
});
|
||||
res2.writeHead(proxyRes.statusCode, proxyRes.headers);
|
||||
res2.end(replaced);
|
||||
} catch (e) {
|
||||
res2.writeHead(proxyRes.statusCode, proxyRes.headers);
|
||||
res2.end();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
res2.writeHead(proxyRes.statusCode, proxyRes.headers);
|
||||
proxyRes.pipe(res2);
|
||||
}
|
||||
} else {
|
||||
// Not masking: just forward the response with frame headers removed
|
||||
res2.writeHead(proxyRes.statusCode, proxyRes.headers);
|
||||
proxyRes.pipe(res2);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -362,7 +417,7 @@ app.use('/', requireLoginForHost, (req, res, next) => {
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 10000,
|
||||
pathRewrite,
|
||||
selfHandleResponse: false,
|
||||
selfHandleResponse: true, // we may rewrite HTML and headers (masking optional)
|
||||
onProxyReq(proxyReq, req, res) {
|
||||
// if we have an access token for this host, supply it
|
||||
if (req.session && req.session.tokens && req.session.tokens[host.id] && req.session.tokens[host.id].access_token) {
|
||||
@ -372,6 +427,73 @@ app.use('/', requireLoginForHost, (req, res, next) => {
|
||||
if (req.session && req.session.user) {
|
||||
proxyReq.setHeader('X-Forwarded-User', req.session.user.username);
|
||||
}
|
||||
},
|
||||
onProxyRes(proxyRes, req2, res2) {
|
||||
// helper: compute proxy origin used by clients
|
||||
const proto = (req2.headers['x-forwarded-proto'] || req2.protocol || 'http');
|
||||
const proxyOrigin = `${proto}://${req2.get('host')}`;
|
||||
// Always remove frame-blocking headers for embedding/preview
|
||||
try {
|
||||
delete proxyRes.headers['x-frame-options'];
|
||||
delete proxyRes.headers['x-content-security-policy'];
|
||||
delete proxyRes.headers['content-security-policy'];
|
||||
delete proxyRes.headers['frame-options'];
|
||||
} catch (e) {}
|
||||
|
||||
// If masking is enabled for this host, perform stronger rewrites
|
||||
if (host.maskUpstream) {
|
||||
// rewrite Location header to point to proxy origin
|
||||
if (proxyRes.headers.location) {
|
||||
try {
|
||||
const loc = proxyRes.headers.location;
|
||||
if (loc.startsWith(host.upstream)) {
|
||||
const newLoc = loc.replace(host.upstream, proxyOrigin + (host.pathPrefix && host.pathPrefix !== '/' ? host.pathPrefix : ''));
|
||||
proxyRes.headers.location = newLoc;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// rewrite Set-Cookie: remove Domain attribute so cookie is set for proxy host
|
||||
if (proxyRes.headers['set-cookie']) {
|
||||
try {
|
||||
const cookies = proxyRes.headers['set-cookie'].map(cookie => cookie.replace(/;\s*Domain=[^;]+/i, ''));
|
||||
proxyRes.headers['set-cookie'] = cookies;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// For HTML responses, buffer and rewrite absolute URLs from upstream to proxy
|
||||
const contentType = (proxyRes.headers['content-type'] || '').toLowerCase();
|
||||
if (contentType.includes('text/html')) {
|
||||
const chunks = [];
|
||||
proxyRes.on('data', chunk => chunks.push(chunk));
|
||||
proxyRes.on('end', () => {
|
||||
try {
|
||||
const body = Buffer.concat(chunks).toString('utf8');
|
||||
const upstreamOrigin = host.upstream.replace(/\/$/, '');
|
||||
const from = new RegExp(upstreamOrigin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
|
||||
const prefix = (host.pathPrefix && host.pathPrefix !== '/') ? host.pathPrefix.replace(/\/$/, '') : '';
|
||||
const replaced = body.replace(from, proxyOrigin + prefix);
|
||||
// set headers and send
|
||||
Object.keys(proxyRes.headers).forEach(h => {
|
||||
if (h.toLowerCase() === 'content-length') delete proxyRes.headers[h];
|
||||
});
|
||||
res2.writeHead(proxyRes.statusCode, proxyRes.headers);
|
||||
res2.end(replaced);
|
||||
} catch (e) {
|
||||
res2.writeHead(proxyRes.statusCode, proxyRes.headers);
|
||||
res2.end();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// non-html: pipe straight through but ensure headers updated
|
||||
res2.writeHead(proxyRes.statusCode, proxyRes.headers);
|
||||
proxyRes.pipe(res2);
|
||||
}
|
||||
} else {
|
||||
// Not masking: just forward the response with frame headers removed
|
||||
res2.writeHead(proxyRes.statusCode, proxyRes.headers);
|
||||
proxyRes.pipe(res2);
|
||||
}
|
||||
}
|
||||
});
|
||||
return proxy(req, res, next);
|
||||
|
||||
@ -35,6 +35,9 @@
|
||||
<input name="oidc_redirect_uri" value="<%= host && host.oidc ? host.oidc.redirect_uri : '' %>" />
|
||||
</label>
|
||||
</fieldset>
|
||||
<label style="margin-top:8px;">
|
||||
<input type="checkbox" name="mask_upstream" <%= host && host.maskUpstream ? 'checked' : '' %> /> Masquer l'upstream (réécrire URLs/ cookies)
|
||||
</label>
|
||||
<div style="margin-top:12px">
|
||||
<button type="submit">Enregistrer</button>
|
||||
</div>
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
<section>
|
||||
<p><a href="/admin/new">Créer un nouvel hôte</a> · <form style="display:inline" method="post" action="/admin/reload"><button type="submit">Reload OIDC</button></form></p>
|
||||
<table>
|
||||
<thead><tr><th>ID</th><th>Prefix</th><th>Upstream</th><th>OIDC</th><th>Actions</th></tr></thead>
|
||||
<thead><tr><th>ID</th><th>Prefix</th><th>Upstream</th><th>OIDC</th><th>Masked</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
<% hosts.forEach(function(h){ %>
|
||||
<tr>
|
||||
@ -23,6 +23,7 @@
|
||||
<td><%= h.pathPrefix %></td>
|
||||
<td><%= h.upstream %></td>
|
||||
<td><%= h.oidc ? 'yes' : 'no' %></td>
|
||||
<td><%= h.maskUpstream ? 'yes' : 'no' %></td>
|
||||
<td class="actions">
|
||||
<a href="/admin/edit/<%= h.id %>">Edit</a>
|
||||
<a href="/admin/preview/<%= h.id %>" target="_blank">Preview</a>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user