fix: resolve Mixed Content (Failed to fetch) in hosted HTTPS environment via /api-wpp reverse proxy
This commit is contained in:
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"rewrites": [
|
||||||
|
{
|
||||||
|
"source": "/api-wpp/:match*",
|
||||||
|
"destination": "http://www.c2wpp.c2sistemas.com.br:22700/:match*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/(.*)",
|
||||||
|
"destination": "/index.html"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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/, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user