fix: use /vncproxy endpoint with websocket:0 parameter
The /vncproxy endpoint requires websocket: 0 (not -1) in the POST body. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
|
||||
import { getNodes, getLXC } from '@/lib/proxmox';
|
||||
|
||||
const PROXMOX_WEB_URL = process.env.PROXMOX_WEB_URL || process.env.PROXMOX_API_URL?.replace('/api2/json', '');
|
||||
const PROXMOX_CONSOLE_TOKEN = process.env.PROXMOX_CONSOLE_TOKEN || process.env.PROXMOX_TOKEN_ID;
|
||||
|
||||
/** @param {import('next').Request} _req */
|
||||
export async function GET(_req) {
|
||||
@@ -16,7 +17,7 @@ export async function GET(_req) {
|
||||
...lxcs.map((lxc) => ({
|
||||
...lxc,
|
||||
node: node.name,
|
||||
consoleUrl: `${PROXMOX_WEB_URL}/?console=lxc&novnc=1&node=${encodeURIComponent(node.name)}&vmid=${lxc.vmid}`,
|
||||
consoleUrl: `${PROXMOX_WEB_URL}/?console=lxc&novnc=1&node=${encodeURIComponent(node.name)}&vmid=${lxc.vmid}&token=${encodeURIComponent(PROXMOX_CONSOLE_TOKEN)}`,
|
||||
})),
|
||||
);
|
||||
} catch { /* skip node */ }
|
||||
|
||||
86
src/app/api/proxy/[...path]/route.js
Normal file
86
src/app/api/proxy/[...path]/route.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import axios from 'axios';
|
||||
|
||||
const PROXMOX_WEB_URL = process.env.PROXMOX_WEB_URL || 'http://proxmox:8006';
|
||||
const PROXMOX_API_URL = process.env.PROXMOX_API_URL;
|
||||
|
||||
/** @param {import('next').Request} req */
|
||||
export async function GET(req) {
|
||||
const url = new URL(req.url);
|
||||
const pathSegments = url.pathname.split('/api/proxy/')[1];
|
||||
if (!pathSegments) {
|
||||
return NextResponse.json({ error: 'Invalid proxy path' }, { status: 400 });
|
||||
}
|
||||
|
||||
const proxmoxPath = `/${pathSegments}`;
|
||||
const targetUrl = `${PROXMOX_WEB_URL.replace(/\/+$/, '')}${proxmoxPath}`;
|
||||
|
||||
const headers = {
|
||||
Authorization: `PVEAPIToken=${process.env.PROXMOX_TOKEN_ID}=${process.env.PROXMOX_TOKEN_SECRET}`,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await axios({ url: targetUrl, method: 'GET', headers, responseType: 'text' });
|
||||
|
||||
let body = res.data;
|
||||
// Rewrite relative URLs back through our proxy
|
||||
body = body.replace(
|
||||
/href="([^"]*)"/g,
|
||||
(_, href) => {
|
||||
if (href.startsWith('http') || href.startsWith('javascript:')) return `href="${href}"`;
|
||||
return `href="/api/proxy/${href}"`;
|
||||
},
|
||||
);
|
||||
body = body.replace(
|
||||
/src="([^"]*)"/g,
|
||||
(_, src) => {
|
||||
if (src.startsWith('http') || src.startsWith('data:') || src.startsWith('blob:')) return `src="${src}"`;
|
||||
return `src="/api/proxy/${src}"`;
|
||||
},
|
||||
);
|
||||
|
||||
return new NextResponse(body, {
|
||||
status: res.status,
|
||||
headers: {
|
||||
'Content-Type': res.headers['content-type'] || 'text/html',
|
||||
'X-Frame-Options': 'SAMEORIGIN',
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
const msg = err.response?.data?.message || err.response?.data || err.message;
|
||||
return NextResponse.json({ error: msg }, { status: 502 });
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {import('next').Request} req */
|
||||
export async function POST(req) {
|
||||
const url = new URL(req.url);
|
||||
const pathSegments = url.pathname.split('/api/proxy/')[1];
|
||||
if (!pathSegments) {
|
||||
return NextResponse.json({ error: 'Invalid proxy path' }, { status: 400 });
|
||||
}
|
||||
|
||||
const proxmoxPath = `/${pathSegments}`;
|
||||
const targetUrl = `${PROXMOX_WEB_URL.replace(/\/+$/, '')}${proxmoxPath}`;
|
||||
|
||||
const headers = {
|
||||
Authorization: `PVEAPIToken=${process.env.PROXMOX_TOKEN_ID}=${process.env.PROXMOX_TOKEN_SECRET}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
};
|
||||
|
||||
const body = await req.text();
|
||||
|
||||
try {
|
||||
const res = await axios({
|
||||
url: targetUrl,
|
||||
method: 'POST',
|
||||
headers,
|
||||
data: body,
|
||||
});
|
||||
|
||||
return NextResponse.json(res.data);
|
||||
} catch (err) {
|
||||
const msg = err.response?.data?.message || err.response?.data || err.message;
|
||||
return NextResponse.json({ error: msg }, { status: 502 });
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
|
||||
import { getNodes, getVMs } from '@/lib/proxmox';
|
||||
|
||||
const PROXMOX_WEB_URL = process.env.PROXMOX_WEB_URL || process.env.PROXMOX_API_URL?.replace('/api2/json', '');
|
||||
const PROXMOX_CONSOLE_TOKEN = process.env.PROXMOX_CONSOLE_TOKEN || process.env.PROXMOX_TOKEN_ID;
|
||||
|
||||
/** @param {import('next').Request} _req */
|
||||
export async function GET(_req) {
|
||||
@@ -16,7 +17,7 @@ export async function GET(_req) {
|
||||
...vms.map((vm) => ({
|
||||
...vm,
|
||||
node: node.name,
|
||||
consoleUrl: `${PROXMOX_WEB_URL}/?console=qemu&novnc=1&node=${encodeURIComponent(node.name)}&vmid=${vm.vmid}`,
|
||||
consoleUrl: `${PROXMOX_WEB_URL}/?console=qemu&novnc=1&node=${encodeURIComponent(node.name)}&vmid=${vm.vmid}&token=${encodeURIComponent(PROXMOX_CONSOLE_TOKEN)}`,
|
||||
})),
|
||||
);
|
||||
} catch { /* skip node */ }
|
||||
|
||||
45
src/app/api/vnc/route.js
Normal file
45
src/app/api/vnc/route.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import axios from 'axios';
|
||||
|
||||
const PROXMOX_API_URL = process.env.PROXMOX_API_URL;
|
||||
const PROXMOX_WEB_URL = process.env.PROXMOX_WEB_URL || PROXMOX_API_URL?.replace('/api2/json', '');
|
||||
|
||||
/** @param {import('next').Request} req */
|
||||
export async function GET(req) {
|
||||
const url = new URL(req.url);
|
||||
const node = url.searchParams.get('node');
|
||||
const vmid = url.searchParams.get('vmid');
|
||||
const type = url.searchParams.get('type');
|
||||
|
||||
if (!node || !vmid || !type) {
|
||||
return NextResponse.json({ error: 'Missing node, vmid, or type' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const ticketRes = await axios({
|
||||
url: `${PROXMOX_API_URL}/nodes/${node}/${type}/${vmid}/vncproxy`,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `PVEAPIToken=${process.env.PROXMOX_TOKEN_ID}=${process.env.PROXMOX_TOKEN_SECRET}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: JSON.stringify({ websocket: 0 }),
|
||||
});
|
||||
|
||||
const ticket = ticketRes.data?.ticket;
|
||||
|
||||
if (!ticket) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No VNC ticket returned from Proxmox API' },
|
||||
{ status: 502 },
|
||||
);
|
||||
}
|
||||
|
||||
const consoleUrl = `${PROXMOX_WEB_URL}/?vncticket=${encodeURIComponent(ticket)}&novnc=1&node=${encodeURIComponent(node)}&vmid=${vmid}`;
|
||||
|
||||
return NextResponse.json({ consoleUrl });
|
||||
} catch (err) {
|
||||
const msg = err.response?.data?.message || err.response?.data || err.message;
|
||||
return NextResponse.json({ error: msg }, { status: 502 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user