1+ import json
12import pickle
3+ from time import time
4+
25import requests
36import urllib3
47
5- from .utils import get_post_form , generate_token
6- from .. import Signer , __version__
8+ from .utils import get_post_form , generate_token , request_ref_headers , request_verification_token , random_ajax_id
9+ from .. import Signer , ResponseError
710
811CONSTANCIA_URL = 'https://rfcampc.siat.sat.gob.mx/PTSC/IdcSiat/IdcGeneraConstancia.jsf'
9- USER_AGENT = __version__ . __user_agent__
12+ USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36'
1013
1114DEFAULT_HEADERS = {
12- 'accept ' : 'text/html,application/xhtml+xml,application/xml' ,
13- 'pragma ' : 'no-cache' ,
14- 'cache-control ' : 'no-cache' ,
15- 'user-agent ' : USER_AGENT
15+ 'Accept ' : 'text/html,application/xhtml+xml,application/xml' ,
16+ 'Pragma ' : 'no-cache' ,
17+ 'Cache-Control ' : 'no-cache' ,
18+ 'User-Agent ' : USER_AGENT
1619}
1720
1821
19- def debug_response (res ):
20- print ('-- RESPONSE DEBUG --' )
21- print (res .status_code )
22- for k , v in res .request .headers .items ():
23- print (k , v )
24- print (res .request .body )
25- print ('results' )
26- print (res .text )
27- print ('-- RESPONSE DEBUG END --' )
28-
29-
30- class SATSession :
22+ class PortalManager (requests .Session ):
3123 def __init__ (self , signer : Signer ):
24+ super ().__init__ ()
3225 urllib3 .util .ssl_ .DEFAULT_CIPHERS += ':HIGH:!DH'
3326 self .signer = signer
34- self .session = requests .session ()
3527
36- def login (self ):
37- LOGIN_URL = 'https://loginda.siat.sat.gob.mx/nidp/app/login?id=fiel'
38- LOGIN_URL_ORIGIN = 'https://loginda.siat.sat.gob.mx'
28+ def save_session (self , target ):
29+ pickle .dump (self .cookies , target )
3930
40- res = self .session .get (
41- url = LOGIN_URL ,
42- headers = DEFAULT_HEADERS
43- )
44- assert res .status_code == 200
31+ def load_session (self , source ):
32+ self .cookies .update (pickle .load (source ))
4533
46- action , data = get_post_form ( res )
47- res = self .session . post (
34+ def form_request ( self , action , referer_url , data ):
35+ res = self .post (
4836 url = action ,
49- headers = DEFAULT_HEADERS | {
50- 'origin' : LOGIN_URL_ORIGIN ,
51- 'referer' : LOGIN_URL ,
52- },
37+ headers = DEFAULT_HEADERS | request_ref_headers (referer_url ),
5338 data = data
5439 )
5540 assert res .status_code == 200
41+ return res
5642
57- action , data = get_post_form (res , id = 'certform' )
58- res = self .session .post (
59- url = action ,
60- headers = DEFAULT_HEADERS | {
61- 'origin' : LOGIN_URL_ORIGIN ,
62- 'referer' : LOGIN_URL ,
63- },
64- data = data | {
43+ def fiel_login (self , login_response ):
44+ action , data = get_post_form (login_response , id = 'certform' )
45+ return self .form_request (
46+ action ,
47+ login_response .request .url ,
48+ data | {
6549 'token' : generate_token (self .signer , code = data ['guid' ]),
6650 'fert' : self .signer .certificate .get_notAfter ()[2 :].decode (),
6751 }
6852 )
53+
54+
55+ class SATPortal (PortalManager ):
56+ def login (self ):
57+ LOGIN_URL = 'https://loginda.siat.sat.gob.mx/nidp/app/login?id=fiel'
58+
59+ res = self .get (
60+ url = LOGIN_URL ,
61+ headers = DEFAULT_HEADERS
62+ )
6963 assert res .status_code == 200
7064
71- return res
65+ action , data = get_post_form (res )
66+ return self .fiel_login (
67+ login_response = self .form_request (action , res .request .url , data )
68+ )
7269
7370 def home_page (self ):
74- return self .session . get (
71+ return self .get (
7572 url = 'https://loginda.siat.sat.gob.mx/nidp/app?sid=0' ,
7673 headers = DEFAULT_HEADERS
7774 )
7875
7976 def logout (self ):
80- return self .session . get (
77+ return self .get (
8178 url = 'https://loginda.siat.sat.gob.mx/nidp/app/logout' ,
8279 headers = DEFAULT_HEADERS | {
8380 'referer' : 'https://loginda.siat.sat.gob.mx/nidp/app?sid=0'
@@ -86,27 +83,146 @@ def logout(self):
8683 )
8784
8885 def declaraciones_provisionales_login (self ):
89- res = self .session . get (
90- url = 'https://ptscdecprov.clouda.sat.gob.mx/ ' ,
86+ res = self .get (
87+ url = 'https://ptscdecprov.clouda.sat.gob.mx' ,
9188 headers = DEFAULT_HEADERS ,
9289 allow_redirects = True
9390 )
9491 assert res .status_code == 200
9592
9693 action , data = get_post_form (res )
97- res = self .session .post (
98- url = action ,
94+ res = self .form_request (action , res .request .url , data )
95+ return res
96+
97+
98+ class SATFacturaElectronica (PortalManager ):
99+ BASE_URL = 'https://portal.facturaelectronica.sat.gob.mx'
100+ REQUEST_CONTEXT = 'appId=cid-v1:20ff76f4-0bca-495f-b7fd-09ca520e39f7'
101+
102+ def __init__ (self , signer : Signer ):
103+ super ().__init__ (signer )
104+ self ._ajax_id = random_ajax_id ()
105+ self ._request_verification_token = None
106+
107+ def login (self ):
108+ res = self .get (
109+ url = self .BASE_URL ,
110+ headers = DEFAULT_HEADERS
111+ )
112+ assert res .status_code == 200
113+
114+ try :
115+ action , data = get_post_form (res )
116+ except IndexError as ex :
117+ raise ValueError ("Login form not found, please try again" ) from ex
118+
119+ if action .startswith ('https://cfdiau.sat.gob.mx/' ):
120+ assert 'nidp/wsfed/ep?id=SATUPCFDiCon' in action
121+
122+ res = self .fiel_login (
123+ login_response = self .form_request (
124+ action .replace ('nidp/wsfed/ep?id=SATUPCFDiCon' , 'nidp/app/login?id=SATx509Custom' ),
125+ res .request .url ,
126+ data
127+ )
128+ )
129+
130+ action , data = get_post_form (res )
131+ res = self .form_request (action , res .request .url , data )
132+
133+ action , data = get_post_form (res )
134+
135+ res = self .form_request (action , res .request .url , data )
136+
137+ self ._request_verification_token = request_verification_token (res )
138+ self ._ajax_id = random_ajax_id ()
139+ return res
140+
141+ def _reload_verification_token (self ):
142+ res = self .get (
143+ url = f'{ self .BASE_URL } /Factura/GeneraFactura' ,
144+ headers = DEFAULT_HEADERS ,
145+ allow_redirects = False
146+ )
147+ if res .status_code == 200 :
148+ self ._request_verification_token = request_verification_token (res )
149+ else :
150+ raise ValueError ('Please Login Again' )
151+
152+ def reactivate_session (self ):
153+ res = self .post (
154+ url = f'{ self .BASE_URL } /Home/ReActiveSession' ,
99155 headers = DEFAULT_HEADERS | {
100- 'content-type ' : 'application/x-www-form-urlencoded ' ,
101- 'origin ' : 'https://loginda.siat.sat.gob.mx' ,
102- 'referer ' : 'https://loginda.siat.sat.gob.mx/' ,
156+ 'Origin ' : f' { self . BASE_URL } ' ,
157+ 'Request-Context ' : self . REQUEST_CONTEXT ,
158+ 'Request-Id ' : f'| { self . _ajax_id } . { random_ajax_id () } '
103159 },
104- data = data
160+ allow_redirects = False
105161 )
106162 return res
107163
108- def save_session (self , target ):
109- pickle .dump (self .session .cookies , target )
164+ def _request (self , method , path , data = None , params = None ):
165+ if self ._request_verification_token is None :
166+ self ._reload_verification_token ()
110167
111- def load_session (self , source ):
112- self .session .cookies .update (pickle .load (source ))
168+ if method .upper () == 'POST' :
169+ headers = {
170+ 'Content-Type' : 'application/x-www-form-urlencoded; charset=UTF-8'
171+ }
172+ else :
173+ headers = {}
174+
175+ res = self .request (
176+ method = method ,
177+ url = f'{ self .BASE_URL } /{ path } ' ,
178+ headers = DEFAULT_HEADERS | headers | {
179+ 'Origin' : self .BASE_URL ,
180+ 'Authority' : self .BASE_URL ,
181+ 'Request-Context' : self .REQUEST_CONTEXT ,
182+ '__RequestVerificationToken' : self ._request_verification_token ,
183+ 'Request-Id' : f'|{ self ._ajax_id } .{ random_ajax_id ()} ' # |pR4Px.o0yAS
184+ },
185+ data = data ,
186+ params = params ,
187+ allow_redirects = False
188+ )
189+ if res .status_code == 200 :
190+ return res .json ()
191+ else :
192+ raise ResponseError (res )
193+
194+ def legal_name_valid (self , rfc , legal_name ):
195+ res = self ._request (
196+ method = 'POST' ,
197+ path = 'Clientes/ValidaRazonSocialRFC' ,
198+ data = {
199+ 'rfcValidar' : rfc .upper (),
200+ 'razonSocial' : legal_name .upper (),
201+ })
202+ if not res ['exitoso' ]:
203+ raise ResponseError (res )
204+ return res ['resultado' ]
205+
206+ def rfc_valid (self , rfc ):
207+ res = self ._request (
208+ method = 'POST' ,
209+ path = 'Clientes/ExisteLrfc' ,
210+ data = {
211+ 'rfcValidar' : rfc .upper ()
212+ }
213+ )
214+ if not res ['exitoso' ]:
215+ raise ResponseError (res )
216+ return res ['resultado' ]
217+
218+ def lco_details (self , rfc , apply_border_region = True ):
219+ res = self ._request (
220+ method = 'GET' ,
221+ path = 'Clientes/ValidaLco' ,
222+ params = {
223+ 'rfcValidar' : rfc .upper (),
224+ 'aplicaRegionFronteriza' : apply_border_region ,
225+ "_" : int (time () * 1000 )
226+ }
227+ )
228+ return json .loads (res )
0 commit comments