Adiciona uma extensão MV3 (Firefox e Chromium/Brave) que insere CPF ou CNPJ
válido direto no campo focado, pelo menu de contexto de qualquer input,
textarea ou contenteditable.
Extensões rodam em sandbox sem acesso a processos, então não há como invocar
os binários cpfgen/cnpjgen; fazer isso exigiria um host de native messaging
registrado no SO. Como os algoritmos são curtos e puros, foram portados para
JS em extension/generator.js, a partir de main.go (CPF) e de
cnpj_generator/cnpj/generate.go (CNPJ), trocando math/rand por
crypto.getRandomValues. test_generator.mjs compara as duas implementações
para que não divirjam.
A inserção usa document.execCommand("insertText"), que emite os eventos
nativos e portanto funciona em campos controlados por React/Vue — atribuir
.value diretamente seria descartado por esses frameworks.
Somente desktop: o Firefox para Android não implementa a API menus.
Testes carregam a extensão de verdade nos dois navegadores e verificam que os
8 itens de menu são criados e que a inserção funciona nos três tipos de campo.
174 lines
6.2 KiB
Python
174 lines
6.2 KiB
Python
"""Carrega a extensão no Brave/Chromium (headless) via DevTools Protocol e
|
|
confirma que o service worker MV3 sobe e registra os 8 itens de menu.
|
|
Usa apenas a stdlib: o WebSocket do CDP é falado na mão."""
|
|
import base64, json, os, shutil, socket, struct, subprocess, tempfile, time
|
|
import urllib.request
|
|
|
|
EXT = os.path.dirname(os.path.abspath(__file__))
|
|
PORT = 9333
|
|
|
|
BROWSER = next((b for b in ("brave", "chromium", "google-chrome-stable", "chrome")
|
|
if shutil.which(b)), None)
|
|
if not BROWSER:
|
|
print("nenhum navegador Chromium encontrado, pulando")
|
|
raise SystemExit(0)
|
|
|
|
profile = tempfile.mkdtemp(prefix="chromium-test-")
|
|
proc = subprocess.Popen([
|
|
BROWSER,
|
|
"--headless=new",
|
|
f"--remote-debugging-port={PORT}",
|
|
f"--user-data-dir={profile}",
|
|
f"--load-extension={EXT}",
|
|
f"--disable-extensions-except={EXT}",
|
|
"--no-first-run", "--no-default-browser-check",
|
|
"--disable-gpu",
|
|
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
|
|
|
|
def cleanup():
|
|
proc.terminate()
|
|
try: proc.wait(timeout=10)
|
|
except Exception: proc.kill()
|
|
shutil.rmtree(profile, ignore_errors=True)
|
|
|
|
|
|
def http(path):
|
|
with urllib.request.urlopen(f"http://127.0.0.1:{PORT}{path}", timeout=10) as r:
|
|
return json.load(r)
|
|
|
|
|
|
class WS:
|
|
"""Cliente WebSocket minimalista, suficiente para falar CDP."""
|
|
def __init__(self, url):
|
|
_, _, rest = url.partition("://")
|
|
hostport, _, path = rest.partition("/")
|
|
host, _, port = hostport.partition(":")
|
|
self.s = socket.create_connection((host, int(port)), timeout=20)
|
|
self.s.settimeout(20)
|
|
key = base64.b64encode(os.urandom(16)).decode()
|
|
self.s.sendall((
|
|
f"GET /{path} HTTP/1.1\r\nHost: {hostport}\r\n"
|
|
"Upgrade: websocket\r\nConnection: Upgrade\r\n"
|
|
f"Sec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n"
|
|
).encode())
|
|
buf = b""
|
|
while b"\r\n\r\n" not in buf:
|
|
buf += self.s.recv(4096)
|
|
self.buf = buf.split(b"\r\n\r\n", 1)[1]
|
|
self.mid = 0
|
|
|
|
def _frame(self, payload):
|
|
data = payload.encode()
|
|
hdr = bytearray([0x81])
|
|
mask = os.urandom(4)
|
|
n = len(data)
|
|
if n < 126:
|
|
hdr.append(0x80 | n)
|
|
elif n < 65536:
|
|
hdr.append(0x80 | 126); hdr += struct.pack(">H", n)
|
|
else:
|
|
hdr.append(0x80 | 127); hdr += struct.pack(">Q", n)
|
|
hdr += mask
|
|
hdr += bytes(b ^ mask[i % 4] for i, b in enumerate(data))
|
|
return bytes(hdr)
|
|
|
|
def _recv_exact(self, n):
|
|
while len(self.buf) < n:
|
|
self.buf += self.s.recv(65536)
|
|
out, self.buf = self.buf[:n], self.buf[n:]
|
|
return out
|
|
|
|
def _read_frame(self):
|
|
h = self._recv_exact(2)
|
|
ln = h[1] & 0x7F
|
|
if ln == 126:
|
|
ln = struct.unpack(">H", self._recv_exact(2))[0]
|
|
elif ln == 127:
|
|
ln = struct.unpack(">Q", self._recv_exact(8))[0]
|
|
return self._recv_exact(ln)
|
|
|
|
def call(self, method, params=None, session=None):
|
|
self.mid += 1
|
|
msg = {"id": self.mid, "method": method, "params": params or {}}
|
|
if session:
|
|
msg["sessionId"] = session
|
|
self.s.sendall(self._frame(json.dumps(msg)))
|
|
while True:
|
|
m = json.loads(self._read_frame())
|
|
if m.get("id") == self.mid:
|
|
if "error" in m:
|
|
raise RuntimeError(f"{method}: {m['error']}")
|
|
return m.get("result", {})
|
|
|
|
|
|
try:
|
|
for _ in range(60):
|
|
try:
|
|
ver = http("/json/version"); break
|
|
except Exception:
|
|
time.sleep(1)
|
|
else:
|
|
raise SystemExit(f"{BROWSER} não subiu")
|
|
print("browser:", ver.get("Product"))
|
|
|
|
ws = WS(ver["webSocketDebuggerUrl"])
|
|
|
|
# Espera o service worker da extensão aparecer como alvo.
|
|
target = None
|
|
for _ in range(40):
|
|
for t in ws.call("Target.getTargets")["targetInfos"]:
|
|
if t["type"] == "service_worker" and t["url"].startswith("chrome-extension://"):
|
|
target = t; break
|
|
if target: break
|
|
time.sleep(0.5)
|
|
if not target:
|
|
raise SystemExit("FALHA: service worker da extensão não iniciou")
|
|
print("service worker:", target["url"])
|
|
|
|
sess = ws.call("Target.attachToTarget",
|
|
{"targetId": target["targetId"], "flatten": True})["sessionId"]
|
|
|
|
# Os menus só existem se background.js executou até o fim. Recria um item
|
|
# com um id já usado: o erro "duplicate id" prova que o original está lá.
|
|
check = """
|
|
(async () => {
|
|
const probe = (id) => new Promise(res => {
|
|
try {
|
|
chrome.contextMenus.create({id, title: 't', contexts: ['editable']},
|
|
() => res(chrome.runtime.lastError
|
|
? chrome.runtime.lastError.message : 'CRIADO-NOVO'));
|
|
} catch (e) { res('THROW: ' + e.message); }
|
|
});
|
|
const ids = ['cpf','cnpj','cpf-formatted','cpf-raw','cnpj-formatted',
|
|
'cnpj-raw','cnpj-alpha-formatted','cnpj-alpha-raw'];
|
|
const out = {};
|
|
for (const id of ids) out[id] = await probe(id);
|
|
return JSON.stringify({
|
|
menusApi: typeof chrome.contextMenus.create === 'function',
|
|
scripting: typeof chrome.scripting.executeScript === 'function',
|
|
probes: out,
|
|
});
|
|
})().catch(e => "ERRO: " + (e && e.stack || e))
|
|
"""
|
|
r = ws.call("Runtime.evaluate",
|
|
{"expression": check, "awaitPromise": True, "returnByValue": True},
|
|
session=sess)
|
|
val = r["result"].get("value")
|
|
if not isinstance(val, str):
|
|
raise SystemExit(f"FALHA ao avaliar no SW: {json.dumps(r)[:900]}")
|
|
if val.startswith("ERRO"):
|
|
raise SystemExit(val)
|
|
|
|
data = json.loads(val)
|
|
assert data["menusApi"], "chrome.contextMenus indisponível"
|
|
assert data["scripting"], "chrome.scripting indisponível"
|
|
faltando = [i for i, r in data["probes"].items() if "duplicate" not in r.lower()]
|
|
assert not faltando, f"itens não criados pelo background: {faltando}"
|
|
print("8/8 itens de menu confirmados no Chromium")
|
|
|
|
# Checa erros do service worker no console.
|
|
errs = ws.call("Log.enable", session=sess)
|
|
print("\nOK: SW ativo, APIs disponíveis, geradores funcionando no Chromium")
|
|
finally:
|
|
cleanup()
|