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
+67 -6
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,19 +46,23 @@ 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);
try {
const res = await fetch(`${normUrl}/api/${secretKey}/show-all-sessions`, { const res = await fetch(`${normUrl}/api/${secretKey}/show-all-sessions`, {
headers: { 'Authorization': `Bearer ${secretKey}` }, headers: { 'Authorization': `Bearer ${secretKey}` },
signal: AbortSignal.timeout(8000), 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,6 +78,7 @@ 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;
try {
const res = await fetch(url, { const res = await fetch(url, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -62,11 +89,14 @@ export async function startSession(server, sessionName, options = {}) {
}); });
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(); return await res.json();
} catch (err) {
throw new Error(formatFetchError(err, baseUrl));
}
} }
/** /**
@@ -74,6 +104,7 @@ 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);
try {
const res = await fetch(`${baseUrl}/api/${sessionName}/${server.secretKey}/generate-token`, { const res = await fetch(`${baseUrl}/api/${sessionName}/${server.secretKey}/generate-token`, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -88,6 +119,10 @@ export async function generateToken(server, sessionName) {
return data.token || null; return data.token || null;
} }
return null; return null;
} catch (err) {
console.warn('Erro ao gerar token:', formatFetchError(err, baseUrl));
return null;
}
} }
/** /**
@@ -98,9 +133,10 @@ 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);
try {
const res = await fetch(`${baseUrl}/api/${sessionName}/check-connection-session`, { const res = await fetch(`${baseUrl}/api/${sessionName}/check-connection-session`, {
headers: { 'Authorization': `Bearer ${authToken}` }, headers: { 'Authorization': `Bearer ${authToken}` },
signal: AbortSignal.timeout(6000), signal: AbortSignal.timeout(8000),
}); });
if (res.ok) { if (res.ok) {
@@ -108,6 +144,10 @@ export async function checkSessionStatus(server, sessionName) {
return data.status || 'unknown'; return data.status || 'unknown';
} }
return 'error'; return 'error';
} catch (err) {
console.warn('Erro ao verificar status:', formatFetchError(err, baseUrl));
return 'error';
}
} }
/** /**
@@ -118,9 +158,10 @@ 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);
try {
const qrRes = await fetch(`${baseUrl}/api/${sessionName}/qrcode-session`, { const qrRes = await fetch(`${baseUrl}/api/${sessionName}/qrcode-session`, {
headers: { 'Authorization': `Bearer ${authToken}` }, headers: { 'Authorization': `Bearer ${authToken}` },
signal: AbortSignal.timeout(10000), signal: AbortSignal.timeout(12000),
}); });
if (!qrRes.ok) return null; if (!qrRes.ok) return null;
@@ -137,6 +178,10 @@ export async function fetchQRCode(server, sessionName) {
return `data:${ct};base64,${btoa(binary)}`; return `data:${ct};base64,${btoa(binary)}`;
} }
return null; return null;
} catch (err) {
console.warn('Erro ao buscar QR code:', formatFetchError(err, baseUrl));
return null;
}
} }
/** /**
@@ -147,6 +192,7 @@ 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);
try {
const res = await fetch(`${baseUrl}/api/${sessionName}/close-session`, { const res = await fetch(`${baseUrl}/api/${sessionName}/close-session`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
@@ -158,6 +204,9 @@ export async function closeSession(server, sessionName) {
throw new Error(errBody || `HTTP ${res.status}`); throw new Error(errBody || `HTTP ${res.status}`);
} }
return true; return true;
} catch (err) {
throw new Error(formatFetchError(err, baseUrl));
}
} }
/** /**
@@ -168,6 +217,7 @@ 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);
try {
const res = await fetch(`${baseUrl}/api/${sessionName}/logout-session`, { const res = await fetch(`${baseUrl}/api/${sessionName}/logout-session`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
@@ -179,6 +229,9 @@ export async function logoutSession(server, sessionName) {
throw new Error(errBody || `HTTP ${res.status}`); throw new Error(errBody || `HTTP ${res.status}`);
} }
return true; return true;
} catch (err) {
throw new Error(formatFetchError(err, baseUrl));
}
} }
/** /**
@@ -186,6 +239,7 @@ 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);
try {
const res = await fetch(`${baseUrl}/api/${sessionName}/${server.secretKey}/clear-session-data`, { const res = await fetch(`${baseUrl}/api/${sessionName}/${server.secretKey}/clear-session-data`, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -199,6 +253,9 @@ export async function clearSessionData(server, sessionName) {
throw new Error(errBody || `HTTP ${res.status}`); throw new Error(errBody || `HTTP ${res.status}`);
} }
return true; return true;
} catch (err) {
throw new Error(formatFetchError(err, baseUrl));
}
} }
/** /**
@@ -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);
try {
const res = await fetch(`${baseUrl}/api/${server.secretKey}/show-all-sessions`, { const res = await fetch(`${baseUrl}/api/${server.secretKey}/show-all-sessions`, {
headers: { 'Authorization': `Bearer ${server.secretKey}` }, headers: { 'Authorization': `Bearer ${server.secretKey}` },
signal: AbortSignal.timeout(10000), 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/, '')
}
}
}
}) })