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'}, {}]},
]},