"""Peças compartilhadas pelos módulos HTTP: leitura de JSON, gravação atômica e token.""" import hmac import json import os from pathlib import Path from urllib.parse import parse_qs, urlsplit class Conflito(Exception): """Pedido válido, mas o estado atual não permite (vira HTTP 409).""" def origem_valida(h): origin = h.headers.get('Origin') host = h.headers.get('Host', '') return not origin or origin in ('http://' + host, 'https://' + host) def ler_json(h, limite): """Corpo JSON de um POST do painel; ValueError com mensagem legível se inválido.""" if not origem_valida(h): raise ValueError('Origem inválida') if h.headers.get('Content-Type', '').split(';')[0] != 'application/json': raise ValueError('Use JSON') tamanho = int(h.headers.get('Content-Length', '0')) if not 0 < tamanho <= limite: raise ValueError(f'Corpo vazio ou maior que {limite} bytes') dados = json.loads(h.rfile.read(tamanho)) if not isinstance(dados, dict): raise ValueError('Objeto JSON obrigatório') return dados def gravar_atomico(caminho, obj, privado=False): caminho = Path(caminho) tmp = caminho.with_suffix('.tmp') texto = json.dumps(obj, ensure_ascii=False, indent=2) if privado: fd = os.open(str(tmp), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) with os.fdopen(fd, 'w', encoding='utf-8') as f: f.write(texto) else: tmp.write_text(texto, encoding='utf-8') tmp.replace(caminho) if privado: os.chmod(caminho, 0o600) def carregar_token(pasta): """CARINHA_TOKEN do ambiente ou {"token": ...} em seguranca.json; None = sem autenticação.""" token = os.environ.get('CARINHA_TOKEN', '').strip() if not token: try: token = str(json.loads((Path(pasta) / 'seguranca.json').read_text(encoding='utf-8')).get('token', '')).strip() except (OSError, ValueError, AttributeError): token = '' if token and len(token) < 8: raise ValueError('CARINHA_TOKEN precisa ter pelo menos 8 caracteres') return token or None def token_do_pedido(h): auth = h.headers.get('Authorization', '') if auth.lower().startswith('bearer '): return auth[7:].strip() if h.headers.get('X-Carinha-Token'): return h.headers['X-Carinha-Token'].strip() # EventSource e o app Android não mandam cabeçalhos próprios com facilidade. return parse_qs(urlsplit(h.path).query).get('t', [''])[0] def autorizado(h): esperado = getattr(h.server, 'token', None) if not esperado: return True return hmac.compare_digest(token_do_pedido(h).encode(), esperado.encode())