primer commit
This commit is contained in:
commit
2be35d7e55
17 files changed
+2709
No files matched your search
+307
@@ -0,0 +1,307 @@
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
import base64
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
import requests
|
||||
import urllib3
|
||||
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
load_dotenv()
|
||||
|
||||
TOKEN_FILE = Path(__file__).parent / "token.json"
|
||||
CLIENT_ID = "cd8e7afb-a0e2-4217-9187-88206e4ba7af"
|
||||
TOKEN_URL = f"https://api-seguridad.sunat.gob.pe/v1/clientessol/{CLIENT_ID}/oauth2/token/"
|
||||
|
||||
RUC = os.getenv("SUNAT_RUC", "").strip()
|
||||
USER = os.getenv("SUNAT_USER", "").strip()
|
||||
PASSWORD = os.getenv("SUNAT_PASSWORD", "").strip()
|
||||
J_USERNAME = USER
|
||||
|
||||
|
||||
def log(msg):
|
||||
print(f" {msg}")
|
||||
|
||||
|
||||
def decode_jwt(token):
|
||||
try:
|
||||
payload_b64 = token.split(".")[1]
|
||||
padding = 4 - len(payload_b64) % 4
|
||||
if padding != 4:
|
||||
payload_b64 += "=" * padding
|
||||
return json.loads(base64.b64decode(payload_b64))
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def save_token(token):
|
||||
payload = decode_jwt(token) or {}
|
||||
expires_at = payload.get("exp", 0)
|
||||
data = {
|
||||
"token": token,
|
||||
"expires_at": expires_at,
|
||||
"client_id": payload.get("clientId", ""),
|
||||
"ruc": payload.get("sub", ""),
|
||||
"usuario_sol": payload.get("userdata", {}).get("usuarioSOL", ""),
|
||||
"_source": "login_api.py",
|
||||
"_grant_type": payload.get("grantType", ""),
|
||||
}
|
||||
TOKEN_FILE.write_text(json.dumps(data, indent=2))
|
||||
print(f"\n✅ Token guardado en: {TOKEN_FILE}")
|
||||
print(f" Expira: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(expires_at))}")
|
||||
print(f" RUC: {data['ruc']}")
|
||||
print(f" Usuario SOL: {data['usuario_sol']}")
|
||||
return True
|
||||
|
||||
|
||||
def try_password_grant():
|
||||
print("\n🔑 Intentando grant_type=password...")
|
||||
data = {
|
||||
"grant_type": "password",
|
||||
"username": J_USERNAME,
|
||||
"password": PASSWORD,
|
||||
"client_id": CLIENT_ID,
|
||||
}
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
try:
|
||||
r = requests.post(TOKEN_URL, data=data, headers=headers, timeout=30, verify=False)
|
||||
print(f" {r.status_code}")
|
||||
if r.status_code == 200:
|
||||
j = r.json()
|
||||
token = j.get("access_token")
|
||||
if token:
|
||||
print(f" ✅ Password grant funcionó!")
|
||||
save_token(token)
|
||||
return True
|
||||
print(f" ⚠️ Sin access_token en respuesta: {str(j)[:200]}")
|
||||
else:
|
||||
print(f" ❌ Response: {r.text[:300]}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def try_auth_code_flow():
|
||||
print("\n🔑 Intentando flujo authorization_code...")
|
||||
log("1. Accediendo a MenuInternet.htm para obtener redirect...")
|
||||
|
||||
session = requests.Session()
|
||||
session.verify = False
|
||||
session.headers.update({
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
})
|
||||
|
||||
menu_url = "https://e-menu.sunat.gob.pe/cl-ti-itmenu/MenuInternet.htm"
|
||||
|
||||
try:
|
||||
r = session.get(menu_url, allow_redirects=True, timeout=30)
|
||||
log(f" GET MenuInternet: {r.status_code}")
|
||||
log(f" URL final: {r.url}")
|
||||
|
||||
html = r.text
|
||||
(Path(__file__).parent / "descargas" / "menu_response.html").write_text(html)
|
||||
|
||||
# Extraer URL del redirect() en el JavaScript
|
||||
redirect_match = re.search(r'redirect\s*\(\s*["\']([^"\']+)["\']', html)
|
||||
if not redirect_match:
|
||||
print(" ❌ No se encontró URL de login en MenuInternet.htm")
|
||||
return False
|
||||
|
||||
authen_url = redirect_match.group(1)
|
||||
log(f"\n Login URL: {authen_url}")
|
||||
|
||||
# Extraer state y redirect_uri de la authen URL
|
||||
state_match = re.search(r'state=([^&\s"\']+)', authen_url)
|
||||
state_from_url = state_match.group(1) if state_match else None
|
||||
if state_from_url:
|
||||
log(f" state: {state_from_url[:60]}...")
|
||||
state_decoded = state_from_url # No decodear: el + de base64 se pierde con unquote
|
||||
|
||||
redirect_uri_match = re.search(r'redirect_uri=([^&\s"\']+)', authen_url)
|
||||
redirect_uri = requests.utils.unquote(redirect_uri_match.group(1)) if redirect_uri_match else None
|
||||
|
||||
# 2. Acceder a la URL de authen para obtener el formulario de login
|
||||
log("\n 2. Accediendo a authen...")
|
||||
r2 = session.get(authen_url, timeout=30)
|
||||
log(f" GET authen: {r2.status_code}")
|
||||
log(f" URL final: {r2.url[:200]}")
|
||||
|
||||
html2 = r2.text
|
||||
(Path(__file__).parent / "descargas" / "login_page.html").write_text(html2)
|
||||
|
||||
# Extraer state y originalUrl de la URL final (authen los pasa a loginMenuSol)
|
||||
final_state_match = re.search(r'[?&]state=([^&\s]+)', r2.url)
|
||||
final_orig_match = re.search(r'[?&]originalUrl=([^&\s]+)', r2.url)
|
||||
final_state = requests.utils.unquote(final_state_match.group(1)) if final_state_match else state_decoded
|
||||
final_orig = requests.utils.unquote(final_orig_match.group(1)) if final_orig_match else redirect_uri
|
||||
|
||||
# Verificar CAPTCHA real: buscar <img id="imgCaptcha" fuera de tags <script>
|
||||
# (el captcha solo se renderiza via JS tras 3 fallos; con requests el JS no corre)
|
||||
html_no_script = re.sub(r'<script[^>]*>.*?</script>', '', html2, flags=re.DOTALL | re.IGNORECASE)
|
||||
if re.search(r'id=[\'"]imgCaptcha[\'"]', html_no_script):
|
||||
print(" ⚠️ CAPTCHA visible detectado (imgCaptcha)")
|
||||
return False
|
||||
print(" ✅ Sin CAPTCHA visible")
|
||||
|
||||
# Extraer form action
|
||||
action_match = re.search(r'<form[^>]*action=[\'"]([^\'"]+)[\'"]', html2)
|
||||
form_action = action_match.group(1) if action_match else "j_security_check"
|
||||
log(f" form action: {form_action}")
|
||||
|
||||
# Construir POST URL (relativo a la URL actual)
|
||||
base_path = r2.url.split("?")[0].rsplit("/", 1)[0] + "/"
|
||||
if form_action.startswith("http"):
|
||||
post_url = form_action
|
||||
elif form_action.startswith("/"):
|
||||
base = re.match(r'(https?://[^/]+)', r2.url)
|
||||
post_url = f"{base.group(1)}{form_action}" if base else form_action
|
||||
else:
|
||||
post_url = base_path + form_action
|
||||
log(f" POST URL: {post_url}")
|
||||
|
||||
# Extraer lang de la URL actual
|
||||
lang_match = re.search(r'lang=([^&\s]+)', r2.url)
|
||||
lang = lang_match.group(1) if lang_match else "es-PE"
|
||||
|
||||
# 3. POST credenciales
|
||||
log("\n 3. Enviando credenciales...")
|
||||
form_data = {
|
||||
"tipo": "2",
|
||||
"dni": "",
|
||||
"custom_ruc": RUC,
|
||||
"j_username": J_USERNAME,
|
||||
"j_password": PASSWORD,
|
||||
"captcha": "",
|
||||
"originalUrl": redirect_uri or "",
|
||||
"lang": lang,
|
||||
"state": state_decoded or "",
|
||||
}
|
||||
|
||||
r3 = session.post(post_url, data=form_data, allow_redirects=False, timeout=30)
|
||||
log(f" POST login: {r3.status_code}")
|
||||
location = r3.headers.get("Location", "")
|
||||
log(f" Location: {location[:200]}")
|
||||
|
||||
# 4. Seguir redirects buscando authorization code
|
||||
auth_code = None
|
||||
next_url = location if r3.status_code in (301, 302, 303, 307, 308) else None
|
||||
|
||||
for i in range(15):
|
||||
if not next_url:
|
||||
break
|
||||
|
||||
if "code=" in next_url:
|
||||
code_match = re.search(r'code=([^&]+)', next_url)
|
||||
if code_match:
|
||||
auth_code = code_match.group(1)
|
||||
log(f"\n ✅ Authorization code obtenido en redirect {i+1}!")
|
||||
break
|
||||
|
||||
r4 = session.get(next_url, allow_redirects=False, timeout=30)
|
||||
log(f" Redirect {i+1}: {r4.status_code} -> {r4.url[:100]}")
|
||||
next_url = r4.headers.get("Location") if r4.status_code in (301, 302, 303, 307, 308) else None
|
||||
|
||||
if auth_code:
|
||||
log(f"\n 4. Siguiendo redirect a AutenticaMenuInternet.htm con session activa...")
|
||||
# El SOL portal intercambia el code internamente y genera el menú
|
||||
# Seguir la cadena de redirects hasta llegar al menú SOL
|
||||
next_url = location
|
||||
for i in range(10):
|
||||
if not next_url:
|
||||
break
|
||||
log(f" GET redirect {i+1}: {next_url[:120]}")
|
||||
r5 = session.get(next_url, allow_redirects=(i == 0), timeout=30)
|
||||
log(f" Status: {r5.status_code}")
|
||||
log(f" URL final: {r5.url[:150]}")
|
||||
|
||||
# Buscar token en la respuesta
|
||||
jwt_matches = re.findall(r'eyJ[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+', r5.text)
|
||||
for j in jwt_matches:
|
||||
if len(j) > 100:
|
||||
print(f"\n ✅ JWT encontrado en HTML!")
|
||||
save_token(j)
|
||||
return True
|
||||
|
||||
# Buscar en headers
|
||||
for h, v in r5.headers.items():
|
||||
if 'token' in h.lower() or 'auth' in h.lower():
|
||||
log(f" Header {h}: {v[:100]}")
|
||||
|
||||
# Buscar en cookies
|
||||
for c in session.cookies:
|
||||
if 'token' in c.name.lower():
|
||||
log(f" Cookie {c.name}: {c.value[:80]}")
|
||||
|
||||
next_url = r5.headers.get("Location") if r5.status_code in (301, 302, 303, 307, 308) else None
|
||||
|
||||
# Si no encontramos token en las respuestas, extraerlo del contenido
|
||||
# Buscar sessionStorage o localStorage hints
|
||||
log(f"\n Buscando token en última respuesta...")
|
||||
patterns = [
|
||||
r'access_token["\']?\s*[=:]\s*["\']([a-zA-Z0-9_\-\.]{100,})["\']',
|
||||
r'token["\']?\s*[=:]\s*["\']([a-zA-Z0-9_\-\.]{100,})["\']',
|
||||
r'["\']SUNAT\.token["\']?\s*[=:]\s*["\']([a-zA-Z0-9_\-\.]+)["\']',
|
||||
]
|
||||
for p in patterns:
|
||||
m = re.search(p, r5.text if 'r5' in dir() else '')
|
||||
if m:
|
||||
print(f"\n ✅ Token encontrado via patrón!")
|
||||
save_token(m.group(1))
|
||||
return True
|
||||
|
||||
print(f"\n ❌ Token no encontrado en respuestas HTTP")
|
||||
print(f" El flujo completo es: login SOL -> menú -> click item -> token en sessionStorage")
|
||||
print(f" Esto requiere JavaScript. Usa: python get_token.py")
|
||||
else:
|
||||
if location:
|
||||
print(f" ❌ No se obtuvo code. Última location: {location[:150]}")
|
||||
else:
|
||||
print(f" ❌ No hubo redirect después del POST")
|
||||
print(f" Respuesta: {r3.text[:500]}")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("🔐 LOGIN SUNAT API (sin Playwright)")
|
||||
print("=" * 60)
|
||||
print(f"\n📋 Config:")
|
||||
print(f" RUC: {RUC}")
|
||||
print(f" Usuario: {USER}")
|
||||
print(f" Client ID: {CLIENT_ID}")
|
||||
print(f" Token URL: {TOKEN_URL}")
|
||||
|
||||
# Intentar password grant primero
|
||||
if try_password_grant():
|
||||
return
|
||||
|
||||
# Si falla, intentar authorization_code flow
|
||||
print("\n⚠️ Password grant falló. Intentando authorization_code flow...")
|
||||
if try_auth_code_flow():
|
||||
return
|
||||
|
||||
print("\n❌ No se pudo obtener token automáticamente.")
|
||||
print(" Posibles causas:")
|
||||
print(" - CAPTCHA requerido (usa: python get_token.py)")
|
||||
print(" - UUID de cliente incorrecto")
|
||||
print(" - Cambios en el flujo OAuth2 de SUNAT")
|
||||
print("\n Usa get_token.py como respaldo: python get_token.py")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user