diff --git a/requirements.txt b/requirements.txt index c17bebb..1afd3ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,9 @@ falcon +falcon-multipart Beaker Mako peewee click logbook bcrypt +python-dateutil diff --git a/source/app/controllers/main.py b/source/app/controllers/main.py index be2322c..ce8bedc 100644 --- a/source/app/controllers/main.py +++ b/source/app/controllers/main.py @@ -70,6 +70,17 @@ class AppValues(object): req.context['result'] = self._db.get_values(table, values) resp.status = falcon.HTTP_200 + def on_post(self, req, resp, table): + file_object = req.get_param('upload') + if file_object is None: + session = req.env['beaker.session'] + values = req.params + req.context['result'] = self._db.validate_cert(values, session) + else: + req.context['result'] = self._db.add_cert(file_object) + resp.status = falcon.HTTP_200 + + class AppPartners(object): diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 12dbe98..4a71ac0 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -2,17 +2,20 @@ import datetime import getpass +import hashlib import json import mimetypes import os import re import sqlite3 +import subprocess +import tempfile import unicodedata import uuid -#~ import bcrypt +from dateutil import parser -from settings import log, template_lookup, COMPANIES, DB_SAT +from settings import DEBUG, log, template_lookup, COMPANIES, DB_SAT #~ def _get_hash(password): @@ -23,6 +26,17 @@ from settings import log, template_lookup, COMPANIES, DB_SAT #~ return bcrypt.hashpw(password.encode(), hashed.encode()) == hashed.encode() +def _call(args): + return subprocess.check_output(args, shell=True).decode() + + +def _save_temp(data, modo='wb'): + path = tempfile.mkstemp()[1] + with open(path, modo) as f: + f.write(data) + return path + + def get_pass(): password = getpass.getpass('Introduce la contraseña: ') pass2 = getpass.getpass('Confirma la contraseña: ') @@ -177,3 +191,148 @@ def to_slug(string): .encode('ascii', 'ignore') .decode('ascii').lower()) return value + + +class Certificado(object): + + def __init__(self, key, cer): + self._key = key + self._cer = cer + self._modulus = '' + self._save_files() + self.error = '' + + def _save_files(self): + try: + self._path_key = _save_temp(self._key) + self._path_cer = _save_temp(self._cer) + except: + self._path_key = '' + self._path_cer = '' + return + + def _kill(self, path): + try: + os.remove(path) + except: + pass + return + + def _get_info_cer(self, session_rfc): + data = {} + args = 'openssl x509 -inform DER -in {}' + try: + cer_pem = _call(args.format(self._path_cer)) + except Exception as e: + self.error = 'No se pudo convertir el CER en PEM' + return data + + args = 'openssl enc -base64 -in {}' + try: + cer_txt = _call(args.format(self._path_cer)) + except Exception as e: + self.error = 'No se pudo convertir el CER en TXT' + return data + + args = 'openssl x509 -inform DER -in {} -noout -{}' + try: + result = _call(args.format(self._path_cer, 'purpose')).split('\n')[3] + except Exception as e: + self.error = 'No se puede saber si es FIEL' + return data + + if result == 'SSL server : No': + self.error = 'El certificado es FIEL' + return data + + result = _call(args.format(self._path_cer, 'serial')) + serie = result.split('=')[1].split('\n')[0][1::2] + result = _call(args.format(self._path_cer, 'subject')) + rfc = result.split('=')[5].split('/')[0].strip() + + if not DEBUG: + if not rfc == session_rfc: + self.error = 'El RFC del certificado no corresponde.' + return data + + dates = _call(args.format(self._path_cer, 'dates')).split('\n') + desde = parser.parse(dates[0].split('=')[1]) + hasta = parser.parse(dates[1].split('=')[1]) + self._modulus = _call(args.format(self._path_cer, 'modulus')) + + data['cer'] = self._cer + data['cer_tmp'] = None + data['cer_pem'] = cer_pem + data['cer_txt'] = cer_txt.replace('\n', '') + data['serie'] = serie + data['rfc'] = rfc + data['desde'] = desde + data['hasta'] = hasta + return data + + def _get_p12(self, password, rfc): + tmp_cer = tempfile.mkstemp()[1] + tmp_key = tempfile.mkstemp()[1] + tmp_p12 = tempfile.mkstemp()[1] + + args = 'openssl x509 -inform DER -in "{}" -out "{}"' + _call(args.format(self._path_cer, tmp_cer)) + args = 'openssl pkcs8 -inform DER -in "{}" -passin pass:{} -out "{}"' + _call(args.format(self._path_key, password, tmp_key)) + + args = 'openssl pkcs12 -export -in "{}" -inkey "{}" -name "{}" -passout ' \ + 'pass:"{}" -out "{}"' + _call(args.format(tmp_cer, tmp_key, rfc, + hashlib.md5(rfc.encode()).hexdigest(), tmp_p12)) + data = open(tmp_p12, 'rb').read() + + self._kill(tmp_cer) + self._kill(tmp_key) + self._kill(tmp_p12) + + return data + + def _get_info_key(self, password, rfc): + data = {} + + args = 'openssl pkcs8 -inform DER -in "{}" -passin pass:{}' + try: + result = _call(args.format(self._path_key, password)) + except Exception as e: + self.error = 'Contraseña incorrecta' + return data + + args = 'openssl pkcs8 -inform DER -in "{}" -passin pass:{} | ' \ + 'openssl rsa -noout -modulus' + mod_key = _call(args.format(self._path_key, password)) + + if self._modulus != mod_key: + self.error = 'Los archivos no son pareja' + return data + + args = 'openssl pkcs8 -inform DER -in "{}" -passin pass:{} | ' \ + 'openssl rsa -des3 -passout pass:{}'.format( + self._path_key, password, hashlib.md5(rfc.encode()).hexdigest()) + key_enc = _call(args) + + data['key'] = self._key + data['key_tmp'] = None + data['key_enc'] = key_enc + data['p12'] = self._get_p12(password, rfc) + return data + + def validate(self, password, rfc): + if not self._path_key or not self._path_cer: + self.error = 'Error al cargar el certificado' + return {} + + data = self._get_info_cer(rfc) + llave = self._get_info_key(password, rfc) + if not llave: + return {} + + data.update(llave) + + self._kill(self._path_key) + self._kill(self._path_cer) + return data diff --git a/source/app/main.py b/source/app/main.py index 47a2530..7f00a2d 100644 --- a/source/app/main.py +++ b/source/app/main.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import falcon +from falcon_multipart.middleware import MultipartMiddleware from beaker.middleware import SessionMiddleware from middleware import ( @@ -20,8 +21,12 @@ from settings import DEBUG db = StorageEngine() -api = falcon.API( - middleware=[AuthMiddleware(), JSONTranslator(), ConnectionMiddleware()]) +api = falcon.API(middleware=[ + AuthMiddleware(), + JSONTranslator(), + ConnectionMiddleware(), + MultipartMiddleware(), +]) api.req_options.auto_parse_form_urlencoded = True api.add_sink(handle_404, '') diff --git a/source/app/models/db.py b/source/app/models/db.py index 123ea77..3a9f4f1 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -14,6 +14,15 @@ class StorageEngine(object): def get_values(self, table, values=None): return getattr(self, '_get_{}'.format(table))(values) + def add_cert(self, file_object): + return main.Certificado.add(file_object) + + def validate_cert(self, values, session): + return main.Certificado.validate(values, session) + + def _get_cert(self, values): + return main.Certificado.get_data() + def _get_cp(self, values): return main.get_cp(values['cp']) diff --git a/source/app/models/main.py b/source/app/models/main.py index 53137f3..52f7e62 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -238,20 +238,77 @@ class Emisor(BaseModel): class Certificado(BaseModel): - key = BlobField() + key = BlobField(null=True) + key_tmp = BlobField(null=True) key_enc = TextField(default='') - cer = BlobField() + cer = BlobField(null=True) + cer_tmp = BlobField(null=True) cer_pem = TextField(default='') cer_txt = TextField(default='') - p12 = BlobField() + p12 = BlobField(null=True) serie = TextField(default='') rfc = TextField(default='') - desde = DateTimeField() - hasta = DateTimeField() + desde = DateTimeField(null=True) + hasta = DateTimeField(null=True) def __str__(self): return self.serie + @classmethod + def get_data(cls): + obj = cls.get_(cls) + row = { + 'cert_rfc': obj.rfc, + 'cert_serie': obj.serie, + 'cert_desde': obj.desde, + 'cert_hasta': obj.hasta, + } + return row + + def get_(cls): + if Certificado.select().count(): + obj = Certificado.select()[0] + else: + obj = Certificado() + return obj + + @classmethod + def add(cls, file_object): + obj = cls.get_(cls) + if file_object.filename.endswith('key'): + obj.key_tmp = file_object.file.read() + elif file_object.filename.endswith('cer'): + obj.cer_tmp = file_object.file.read() + obj.save() + return {'status': 'server'} + + @classmethod + def validate(cls, values, session): + row = {} + result = False + obj = cls.get_(cls) + cert = util.Certificado(obj.key_tmp, obj.cer_tmp) + data = cert.validate(values['contra'], session['rfc']) + if data: + msg = 'Certificado guardado correctamente' + q = Certificado.update(**data).where(Certificado.id==obj.id) + if q.execute(): + result = True + row = { + 'cert_rfc': data['rfc'], + 'cert_serie': data['serie'], + 'cert_desde': data['desde'], + 'cert_hasta': data['hasta'], + } + else: + msg = cert.error + obj.key_tmp = None + obj.cer_tmp = None + obj.save() + + return {'ok': result, 'msg': msg, 'data': row} + + class Folios(BaseModel): serie = TextField(unique=True) diff --git a/source/static/js/controller/admin.js b/source/static/js/controller/admin.js index 361df79..89a026a 100644 --- a/source/static/js/controller/admin.js +++ b/source/static/js/controller/admin.js @@ -13,7 +13,8 @@ var controllers = { $$('emisor_cp').attachEvent('onTimedKeyPress', emisor_postal_code_key_up) $$('chk_escuela').attachEvent('onChange', chk_escuela_change) $$('chk_ong').attachEvent('onChange', chk_ong_change) - + $$('cmd_subir_certificado').attachEvent('onItemClick', cmd_subir_certificado_click) + $$('up_cert').attachEvent('onUploadComplete', up_cert_upload_complete) } } @@ -141,11 +142,29 @@ function get_emisor(){ } +function get_certificado(){ + var form = $$('form_cert') + + webix.ajax().get("/values/cert", {}, { + error: function(text, data, xhr) { + msg = 'Error al consultar' + msg_error(msg) + }, + success: function(text, data, xhr) { + var values = data.json() + form.setValues(values) + } + }) + +} + + function multi_admin_change(prevID, nextID){ //~ webix.message(nextID) if(nextID == 'app_emisor'){ $$('tab_emisor').setValue('Datos Fiscales') get_emisor() + get_certificado() return } @@ -221,3 +240,99 @@ function chk_ong_change(new_value, old_value){ $$('ong_fecha_dof').disable() } } + + +function cmd_subir_certificado_click(){ + var form = $$('form_upload') + + if (!form.validate()){ + msg = 'Valores inválidos' + msg_error(msg) + return + } + + var values = form.getValues() + + if(!values.contra.trim()){ + msg = 'La contraseña no puede estar vacía' + msg_error(msg) + return + } + + if($$('lst_cert').count() < 2){ + msg = 'Selecciona al menos dos archivos: CER y KEY del certificado.' + msg_error(msg) + return + } + + if($$('lst_cert').count() > 2){ + msg = 'Selecciona solo dos archivos: CER y KEY del certificado.' + msg_error(msg) + return + } + + var fo1 = $$('up_cert').files.getItem($$('up_cert').files.getFirstId()) + var fo2 = $$('up_cert').files.getItem($$('up_cert').files.getLastId()) + + var ext = ['key', 'cer'] + if(ext.indexOf(fo1.type.toLowerCase()) == -1 || ext.indexOf(fo2.type.toLowerCase()) == -1){ + msg = 'Archivos inválidos, se requiere un archivo CER y un KEY.' + msg_error(msg) + return + } + + if(fo1.type == fo2.type && fo1.size == fo2.size){ + msg = 'Selecciona archivos diferentes: un archivo CER y un KEY.' + msg_error(msg) + return + } + + var rfc = $$('form_cert').getValues()['cert_rfc'] + if(rfc){ + msg = 'Ya existe un certificado guardado

¿Deseas reemplazarlo?' + webix.confirm({ + title: 'Certificado Existente', + ok: 'Si', + cancel: 'No', + type: 'confirm-error', + text: msg, + callback:function(result){ + if(result){ + $$('up_cert').send() + } + } + }) + } +} + + +function up_cert_upload_complete(response){ + if(response.status != 'server'){ + msg = 'Ocurrio un error al subir los archivos' + msg_error(msg) + return + } + + msg = 'Archivos subidos correctamente. Esperando validación' + msg_sucess(msg) + + var values = $$('form_upload').getValues() + $$('form_upload').setValues({}) + $$('up_cert').files.data.clearAll() + + webix.ajax().post('/values/cert', values, { + error:function(text, data, XmlHttpRequest){ + msg = 'Ocurrio un error, consulta a soporte técnico' + msg_error(msg) + }, + success:function(text, data, XmlHttpRequest){ + var values = data.json() + if(values.ok){ + $$('form_cert').setValues(values.data) + msg_sucess(values.msg) + }else{ + msg_error(values.msg) + } + } + }) +} diff --git a/source/static/js/ui/admin.js b/source/static/js/ui/admin.js index 9f45d8e..d064eef 100644 --- a/source/static/js/ui/admin.js +++ b/source/static/js/ui/admin.js @@ -104,15 +104,15 @@ var emisor_certificado = [ {template: 'Cargar Certificado', type: 'section'}, {view: 'form', id: 'form_upload', rows: [ {cols: [{}, - {view: 'uploader', id: 'up_cert', autosend:false, link: 'lst_cert', - value: 'Seleccionar certificado', upload: '/values/cert'}, {}]}, + {view: 'uploader', id: 'up_cert', autosend: false, link: 'lst_cert', + value: 'Seleccionar certificado', upload: '/values/files'}, {}]}, {cols: [{}, - {view: 'list', id: 'lst_cert', type: 'uploader', autoheight:true, - borderless: true}, {}]}, + {view: 'list', id: 'lst_cert', name: 'certificado', + type: 'uploader', autoheight:true, borderless: true}, {}]}, {cols: [{}, - {view: 'text', id: 'txt_contra', label: 'Contraseña KEY', - labelPosition: 'top', labelAlign: 'center', type: 'password', - required: true}, {}]}, + {view: 'text', id: 'txt_contra', name: 'contra', + label: 'Contraseña KEY', labelPosition: 'top', + labelAlign: 'center', type: 'password', required: true}, {}]}, {cols: [{}, {view: 'button', id: 'cmd_subir_certificado', label: 'Subir certificado'}, {}]}, ]},