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;
}
# 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
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
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) {
if (!url) return '';
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;
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) {
if (!qrCode) return '';
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) {
const normUrl = normalizeUrl(baseUrl);
const res = await fetch(`${normUrl}/api/${secretKey}/show-all-sessions`, {
headers: { 'Authorization': `Bearer ${secretKey}` },
signal: AbortSignal.timeout(8000),
});
try {
const res = await fetch(`${normUrl}/api/${secretKey}/show-all-sessions`, {
headers: { 'Authorization': `Bearer ${secretKey}` },
signal: AbortSignal.timeout(10000),
});
if (res.ok) {
return { ok: true, message: 'Conexão OK! Servidor respondeu com sucesso.' };
} else if (res.status === 401) {
return { ok: false, error: 'Token incorreto. Verifique a Secret Key.' };
} else {
const text = await res.text().catch(() => '');
return { ok: false, error: `Erro ${res.status}: ${text || 'Resposta inesperada'}` };
if (res.ok) {
return { ok: true, message: 'Conexão OK! Servidor WPPConnect respondeu com sucesso.' };
} else if (res.status === 401) {
return { ok: false, error: 'Token incorreto. Verifique a Secret Key.' };
} else {
const text = await res.text().catch(() => '');
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?.proxy) body.proxy = session.proxy;
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`,
},
body: JSON.stringify(body),
});
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`,
},
body: JSON.stringify(body),
});
if (!res.ok) {
const errText = await res.text();
throw new Error(errText || `HTTP ${res.status}`);
if (!res.ok) {
const errText = await res.text().catch(() => '');
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) {
const baseUrl = normalizeUrl(server.baseUrl);
const res = await fetch(`${baseUrl}/api/${sessionName}/${server.secretKey}/generate-token`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${server.secretKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ session: sessionName }),
});
try {
const res = await fetch(`${baseUrl}/api/${sessionName}/${server.secretKey}/generate-token`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${server.secretKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ session: sessionName }),
});
if (res.ok) {
const data = await res.json();
return data.token || null;
if (res.ok) {
const data = await res.json();
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 authToken = getSessionAuth(server, session);
const res = await fetch(`${baseUrl}/api/${sessionName}/check-connection-session`, {
headers: { 'Authorization': `Bearer ${authToken}` },
signal: AbortSignal.timeout(6000),
});
try {
const res = await fetch(`${baseUrl}/api/${sessionName}/check-connection-session`, {
headers: { 'Authorization': `Bearer ${authToken}` },
signal: AbortSignal.timeout(8000),
});
if (res.ok) {
const data = await res.json();
return data.status || 'unknown';
if (res.ok) {
const data = await res.json();
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 authToken = getSessionAuth(server, session);
const qrRes = await fetch(`${baseUrl}/api/${sessionName}/qrcode-session`, {
headers: { 'Authorization': `Bearer ${authToken}` },
signal: AbortSignal.timeout(10000),
});
try {
const qrRes = await fetch(`${baseUrl}/api/${sessionName}/qrcode-session`, {
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') || '';
if (ct.includes('json')) {
const data = await qrRes.json();
return data.qrcode ? ensureQRPrefix(data.qrcode) : null;
} else if (ct.includes('image')) {
const buf = await qrRes.arrayBuffer();
const bytes = new Uint8Array(buf);
let binary = '';
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
return `data:${ct};base64,${btoa(binary)}`;
const ct = qrRes.headers.get('content-type') || '';
if (ct.includes('json')) {
const data = await qrRes.json();
return data.qrcode ? ensureQRPrefix(data.qrcode) : null;
} else if (ct.includes('image')) {
const buf = await qrRes.arrayBuffer();
const bytes = new Uint8Array(buf);
let binary = '';
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
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);
let authToken = getSessionAuth(server, session);
const res = await fetch(`${baseUrl}/api/${sessionName}/close-session`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
body: JSON.stringify({ secretkey: server.secretKey }),
});
try {
const res = await fetch(`${baseUrl}/api/${sessionName}/close-session`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
body: JSON.stringify({ secretkey: server.secretKey }),
});
if (!res.ok) {
const errBody = await res.text().catch(() => '');
throw new Error(errBody || `HTTP ${res.status}`);
if (!res.ok) {
const errBody = await res.text().catch(() => '');
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);
let authToken = getSessionAuth(server, session);
const res = await fetch(`${baseUrl}/api/${sessionName}/logout-session`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
body: JSON.stringify({ secretkey: server.secretKey }),
});
try {
const res = await fetch(`${baseUrl}/api/${sessionName}/logout-session`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
body: JSON.stringify({ secretkey: server.secretKey }),
});
if (!res.ok) {
const errBody = await res.text().catch(() => '');
throw new Error(errBody || `HTTP ${res.status}`);
if (!res.ok) {
const errBody = await res.text().catch(() => '');
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) {
const baseUrl = normalizeUrl(server.baseUrl);
const res = await fetch(`${baseUrl}/api/${sessionName}/${server.secretKey}/clear-session-data`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${server.secretKey}`,
},
});
try {
const res = await fetch(`${baseUrl}/api/${sessionName}/${server.secretKey}/clear-session-data`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${server.secretKey}`,
},
});
if (!res.ok) {
const errBody = await res.text().catch(() => '');
throw new Error(errBody || `HTTP ${res.status}`);
if (!res.ok) {
const errBody = await res.text().catch(() => '');
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) {
const baseUrl = normalizeUrl(server.baseUrl);
const res = await fetch(`${baseUrl}/api/${server.secretKey}/show-all-sessions`, {
headers: { 'Authorization': `Bearer ${server.secretKey}` },
signal: AbortSignal.timeout(10000),
});
try {
const res = await fetch(`${baseUrl}/api/${server.secretKey}/show-all-sessions`, {
headers: { 'Authorization': `Bearer ${server.secretKey}` },
signal: AbortSignal.timeout(12000),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
return data.response || [];
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
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/
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api-wpp': {
target: 'http://www.c2wpp.c2sistemas.com.br:22700',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api-wpp/, '')
}
}
}
})