fix: resolve Mixed Content (Failed to fetch) in hosted HTTPS environment via /api-wpp reverse proxy

This commit is contained in:
2026-10-15 13:08:16 -03:00
parent bdb781ed01
commit 91eedc220b
6 changed files with 212 additions and 96 deletions
+17
View File
@@ -0,0 +1,17 @@
[build]
publish = "dist"
command = "npm run build"
# Proxy Reverso para a API WPPConnect C2 Sistemas
# Resolve o bloqueio de "Mixed Content" (Página HTTPS chamando API HTTP na porta 22700)
[[redirects]]
from = "/api-wpp/*"
to = "http://www.c2wpp.c2sistemas.com.br:22700/:splat"
status = 200
force = true
# Roteamento Single Page Application (SPA)
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
+11
View File
@@ -24,6 +24,17 @@ server {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }
# Proxy reverso para WPPConnect (Resolve Mixed Content HTTPS -> HTTP)
location /api-wpp/ {
proxy_pass http://www.c2wpp.c2sistemas.com.br:22700/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host www.c2wpp.c2sistemas.com.br:22700;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 90s;
}
# Cache de assets estáticos # Cache de assets estáticos
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y; expires 1y;
+6
View File
@@ -0,0 +1,6 @@
# Proxy reverso para a API WPPConnect
# Permite que aplicações hospedadas em HTTPS façam chamadas para o servidor HTTP :22700 sem bloqueio de Mixed Content
/api-wpp/* http://www.c2wpp.c2sistemas.com.br:22700/:splat 200!
# SPA Fallback (Single Page Application - React Router / Vite)
/* /index.html 200
+157 -96
View File
@@ -5,10 +5,32 @@
export function normalizeUrl(url) { export function normalizeUrl(url) {
if (!url) return ''; if (!url) return '';
let u = url.trim().replace(/\/+$/, ''); let u = url.trim().replace(/\/+$/, '');
// Se a página estiver rodando em HTTPS (ex: Netlify, Vercel ou servidor com SSL)
// e a URL apontar para o servidor WPP HTTP padrão (porta 22700), o navegador bloqueia por "Mixed Content".
// Redirecionamos automaticamente para o proxy reverso relativo /api-wpp provido pelo Netlify/Vercel/Nginx.
if (typeof window !== 'undefined' && window.location.protocol === 'https:') {
if (u.includes('c2wpp.c2sistemas.com.br:22700') || u.startsWith('/api-wpp')) {
return `${window.location.origin}/api-wpp`;
}
}
if (!/^https?:\/\//i.test(u)) u = 'http://' + u; if (!/^https?:\/\//i.test(u)) u = 'http://' + u;
return u; return u;
} }
export function formatFetchError(err, baseUrl = '') {
if (!err) return 'Erro desconhecido de conexão.';
const msg = err.message || '';
if (msg.includes('Failed to fetch') || msg.includes('NetworkError') || err.name === 'TypeError') {
if (typeof window !== 'undefined' && window.location.protocol === 'https:' && !baseUrl.includes('/api-wpp')) {
return 'Bloqueio de Conteúdo Misto (Mixed Content): O site está em HTTPS, mas o servidor WPP está em HTTP. O proxy reverso /api-wpp intermediará as requisições.';
}
return 'Falha na conexão com o servidor WPP. Verifique se o servidor está online e acessível.';
}
return msg;
}
export function ensureQRPrefix(qrCode) { export function ensureQRPrefix(qrCode) {
if (!qrCode) return ''; if (!qrCode) return '';
return qrCode.startsWith('data:') ? qrCode : `data:image/png;base64,${qrCode}`; return qrCode.startsWith('data:') ? qrCode : `data:image/png;base64,${qrCode}`;
@@ -24,18 +46,22 @@ export function getSessionAuth(server, session) {
*/ */
export async function testServerConnection(baseUrl, secretKey) { export async function testServerConnection(baseUrl, secretKey) {
const normUrl = normalizeUrl(baseUrl); const normUrl = normalizeUrl(baseUrl);
const res = await fetch(`${normUrl}/api/${secretKey}/show-all-sessions`, { try {
headers: { 'Authorization': `Bearer ${secretKey}` }, const res = await fetch(`${normUrl}/api/${secretKey}/show-all-sessions`, {
signal: AbortSignal.timeout(8000), headers: { 'Authorization': `Bearer ${secretKey}` },
}); signal: AbortSignal.timeout(10000),
});
if (res.ok) { if (res.ok) {
return { ok: true, message: 'Conexão OK! Servidor respondeu com sucesso.' }; return { ok: true, message: 'Conexão OK! Servidor WPPConnect respondeu com sucesso.' };
} else if (res.status === 401) { } else if (res.status === 401) {
return { ok: false, error: 'Token incorreto. Verifique a Secret Key.' }; return { ok: false, error: 'Token incorreto. Verifique a Secret Key.' };
} else { } else {
const text = await res.text().catch(() => ''); const text = await res.text().catch(() => '');
return { ok: false, error: `Erro ${res.status}: ${text || 'Resposta inesperada'}` }; return { ok: false, error: `Erro ${res.status}: ${text || 'Resposta inesperada'}` };
}
} catch (err) {
return { ok: false, error: formatFetchError(err, normUrl) };
} }
} }
@@ -52,21 +78,25 @@ export async function startSession(server, sessionName, options = {}) {
if (session?.webhook) body.webhook = session.webhook; if (session?.webhook) body.webhook = session.webhook;
if (session?.proxy) body.proxy = session.proxy; if (session?.proxy) body.proxy = session.proxy;
const res = await fetch(url, { try {
method: 'POST', const res = await fetch(url, {
headers: { method: 'POST',
'Content-Type': 'application/json', headers: {
'Authorization': `Bearer ${authToken}`, 'Content-Type': 'application/json',
}, 'Authorization': `Bearer ${authToken}`,
body: JSON.stringify(body), },
}); body: JSON.stringify(body),
});
if (!res.ok) { if (!res.ok) {
const errText = await res.text(); const errText = await res.text().catch(() => '');
throw new Error(errText || `HTTP ${res.status}`); throw new Error(errText || `HTTP ${res.status}`);
}
return await res.json();
} catch (err) {
throw new Error(formatFetchError(err, baseUrl));
} }
return await res.json();
} }
/** /**
@@ -74,20 +104,25 @@ export async function startSession(server, sessionName, options = {}) {
*/ */
export async function generateToken(server, sessionName) { export async function generateToken(server, sessionName) {
const baseUrl = normalizeUrl(server.baseUrl); const baseUrl = normalizeUrl(server.baseUrl);
const res = await fetch(`${baseUrl}/api/${sessionName}/${server.secretKey}/generate-token`, { try {
method: 'POST', const res = await fetch(`${baseUrl}/api/${sessionName}/${server.secretKey}/generate-token`, {
headers: { method: 'POST',
'Authorization': `Bearer ${server.secretKey}`, headers: {
'Content-Type': 'application/json', 'Authorization': `Bearer ${server.secretKey}`,
}, 'Content-Type': 'application/json',
body: JSON.stringify({ session: sessionName }), },
}); body: JSON.stringify({ session: sessionName }),
});
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
return data.token || null; return data.token || null;
}
return null;
} catch (err) {
console.warn('Erro ao gerar token:', formatFetchError(err, baseUrl));
return null;
} }
return null;
} }
/** /**
@@ -98,16 +133,21 @@ export async function checkSessionStatus(server, sessionName) {
const session = server.sessions?.find(s => s.name === sessionName); const session = server.sessions?.find(s => s.name === sessionName);
const authToken = getSessionAuth(server, session); const authToken = getSessionAuth(server, session);
const res = await fetch(`${baseUrl}/api/${sessionName}/check-connection-session`, { try {
headers: { 'Authorization': `Bearer ${authToken}` }, const res = await fetch(`${baseUrl}/api/${sessionName}/check-connection-session`, {
signal: AbortSignal.timeout(6000), headers: { 'Authorization': `Bearer ${authToken}` },
}); signal: AbortSignal.timeout(8000),
});
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
return data.status || 'unknown'; return data.status || 'unknown';
}
return 'error';
} catch (err) {
console.warn('Erro ao verificar status:', formatFetchError(err, baseUrl));
return 'error';
} }
return 'error';
} }
/** /**
@@ -118,25 +158,30 @@ export async function fetchQRCode(server, sessionName) {
const session = server.sessions?.find(s => s.name === sessionName); const session = server.sessions?.find(s => s.name === sessionName);
const authToken = getSessionAuth(server, session); const authToken = getSessionAuth(server, session);
const qrRes = await fetch(`${baseUrl}/api/${sessionName}/qrcode-session`, { try {
headers: { 'Authorization': `Bearer ${authToken}` }, const qrRes = await fetch(`${baseUrl}/api/${sessionName}/qrcode-session`, {
signal: AbortSignal.timeout(10000), headers: { 'Authorization': `Bearer ${authToken}` },
}); signal: AbortSignal.timeout(12000),
});
if (!qrRes.ok) return null; if (!qrRes.ok) return null;
const ct = qrRes.headers.get('content-type') || ''; const ct = qrRes.headers.get('content-type') || '';
if (ct.includes('json')) { if (ct.includes('json')) {
const data = await qrRes.json(); const data = await qrRes.json();
return data.qrcode ? ensureQRPrefix(data.qrcode) : null; return data.qrcode ? ensureQRPrefix(data.qrcode) : null;
} else if (ct.includes('image')) { } else if (ct.includes('image')) {
const buf = await qrRes.arrayBuffer(); const buf = await qrRes.arrayBuffer();
const bytes = new Uint8Array(buf); const bytes = new Uint8Array(buf);
let binary = ''; let binary = '';
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]); for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
return `data:${ct};base64,${btoa(binary)}`; return `data:${ct};base64,${btoa(binary)}`;
}
return null;
} catch (err) {
console.warn('Erro ao buscar QR code:', formatFetchError(err, baseUrl));
return null;
} }
return null;
} }
/** /**
@@ -147,17 +192,21 @@ export async function closeSession(server, sessionName) {
const session = server.sessions?.find(s => s.name === sessionName); const session = server.sessions?.find(s => s.name === sessionName);
let authToken = getSessionAuth(server, session); let authToken = getSessionAuth(server, session);
const res = await fetch(`${baseUrl}/api/${sessionName}/close-session`, { try {
method: 'POST', const res = await fetch(`${baseUrl}/api/${sessionName}/close-session`, {
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }, method: 'POST',
body: JSON.stringify({ secretkey: server.secretKey }), headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
}); body: JSON.stringify({ secretkey: server.secretKey }),
});
if (!res.ok) { if (!res.ok) {
const errBody = await res.text().catch(() => ''); const errBody = await res.text().catch(() => '');
throw new Error(errBody || `HTTP ${res.status}`); throw new Error(errBody || `HTTP ${res.status}`);
}
return true;
} catch (err) {
throw new Error(formatFetchError(err, baseUrl));
} }
return true;
} }
/** /**
@@ -168,17 +217,21 @@ export async function logoutSession(server, sessionName) {
const session = server.sessions?.find(s => s.name === sessionName); const session = server.sessions?.find(s => s.name === sessionName);
let authToken = getSessionAuth(server, session); let authToken = getSessionAuth(server, session);
const res = await fetch(`${baseUrl}/api/${sessionName}/logout-session`, { try {
method: 'POST', const res = await fetch(`${baseUrl}/api/${sessionName}/logout-session`, {
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }, method: 'POST',
body: JSON.stringify({ secretkey: server.secretKey }), headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
}); body: JSON.stringify({ secretkey: server.secretKey }),
});
if (!res.ok) { if (!res.ok) {
const errBody = await res.text().catch(() => ''); const errBody = await res.text().catch(() => '');
throw new Error(errBody || `HTTP ${res.status}`); throw new Error(errBody || `HTTP ${res.status}`);
}
return true;
} catch (err) {
throw new Error(formatFetchError(err, baseUrl));
} }
return true;
} }
/** /**
@@ -186,19 +239,23 @@ export async function logoutSession(server, sessionName) {
*/ */
export async function clearSessionData(server, sessionName) { export async function clearSessionData(server, sessionName) {
const baseUrl = normalizeUrl(server.baseUrl); const baseUrl = normalizeUrl(server.baseUrl);
const res = await fetch(`${baseUrl}/api/${sessionName}/${server.secretKey}/clear-session-data`, { try {
method: 'POST', const res = await fetch(`${baseUrl}/api/${sessionName}/${server.secretKey}/clear-session-data`, {
headers: { method: 'POST',
'Content-Type': 'application/json', headers: {
'Authorization': `Bearer ${server.secretKey}`, 'Content-Type': 'application/json',
}, 'Authorization': `Bearer ${server.secretKey}`,
}); },
});
if (!res.ok) { if (!res.ok) {
const errBody = await res.text().catch(() => ''); const errBody = await res.text().catch(() => '');
throw new Error(errBody || `HTTP ${res.status}`); throw new Error(errBody || `HTTP ${res.status}`);
}
return true;
} catch (err) {
throw new Error(formatFetchError(err, baseUrl));
} }
return true;
} }
/** /**
@@ -206,12 +263,16 @@ export async function clearSessionData(server, sessionName) {
*/ */
export async function listAllServerSessions(server) { export async function listAllServerSessions(server) {
const baseUrl = normalizeUrl(server.baseUrl); const baseUrl = normalizeUrl(server.baseUrl);
const res = await fetch(`${baseUrl}/api/${server.secretKey}/show-all-sessions`, { try {
headers: { 'Authorization': `Bearer ${server.secretKey}` }, const res = await fetch(`${baseUrl}/api/${server.secretKey}/show-all-sessions`, {
signal: AbortSignal.timeout(10000), headers: { 'Authorization': `Bearer ${server.secretKey}` },
}); signal: AbortSignal.timeout(12000),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json(); const data = await res.json();
return data.response || []; return data.response || [];
} catch (err) {
throw new Error(formatFetchError(err, baseUrl));
}
} }
+12
View File
@@ -0,0 +1,12 @@
{
"rewrites": [
{
"source": "/api-wpp/:match*",
"destination": "http://www.c2wpp.c2sistemas.com.br:22700/:match*"
},
{
"source": "/(.*)",
"destination": "/index.html"
}
]
}
+9
View File
@@ -4,4 +4,13 @@ import { defineConfig } from 'vite'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: {
proxy: {
'/api-wpp': {
target: 'http://www.c2wpp.c2sistemas.com.br:22700',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api-wpp/, '')
}
}
}
}) })