Commit 6bf00945 by Luciano Barletta

documented

1 parent a0f76e52
# Servicio Mail
Este servicio es capaz de mandar:
texto - 'text'
imagenes - 'image'
todo tipo de archivos - 'document'
Ahora mismo no se puede poner tema al mail y los archivos enviados van sin nombre 'noname'
...@@ -3,9 +3,11 @@ ...@@ -3,9 +3,11 @@
Este repositorio es un servicio web de Flask en Python3 cuyo propósito es mandar mensajes de todo tipo de formatos a todo tipo de plataformas que tengan una API para ese propósito. Este repositorio es un servicio web de Flask en Python3 cuyo propósito es mandar mensajes de todo tipo de formatos a todo tipo de plataformas que tengan una API para ese propósito.
Implementa una base de datos de mensajes a enviar, usa la MAC address del host que pide mandar mensajes para diferenciar las consultas y permitir la posibilidad de que el mensaje venga encriptado. Implementa una base de datos de mensajes a enviar, usa la MAC address del host que pide mandar mensajes para diferenciar las consultas y permitir la posibilidad de que el mensaje venga encriptado.
## Instalación ## Instalación
Los requerimientos del programa son tener Python3 y pip3, para luego instalar los módulos de Flask, sqlite3 y python-arptable. Si no quiere hacerlo manualmente hay un archivo [install.sh](/install.sh). Los requerimientos del programa son tener Python3 y pip3, para luego instalar los módulos de Flask, sqlite3 y python-arptable. Si no quiere hacerlo manualmente hay un archivo [install.sh](/install.sh) (desactualizado).
## Preparación ## Preparación
...@@ -15,6 +17,7 @@ Para correr el servicio en el servidor simplemente hay que escribir en la consol ...@@ -15,6 +17,7 @@ Para correr el servicio en el servidor simplemente hay que escribir en la consol
python3 deploy.py python3 deploy.py
``` ```
## Uso ## Uso
Para enviar mensajes se puede usar dos formas, la forma encriptada o la forma sin encripción, ambas son similares. Para enviar mensajes se puede usar dos formas, la forma encriptada o la forma sin encripción, ambas son similares.
...@@ -28,13 +31,13 @@ Llame a esta ruta para conseguir la llave pública del servidor en caso de quere ...@@ -28,13 +31,13 @@ Llame a esta ruta para conseguir la llave pública del servidor en caso de quere
Este link es usado para subir los datos a enviar, pero obligatoriamente debe estar en formato de archivo. Opcionalmente puede estar encriptado, en cuyo caso debe mandar una llave en formato de archivo, el procedimiento es el siguiente: Este link es usado para subir los datos a enviar, pero obligatoriamente debe estar en formato de archivo. Opcionalmente puede estar encriptado, en cuyo caso debe mandar una llave en formato de archivo, el procedimiento es el siguiente:
- Crear una llave simétrica AES - Crear una llave simétrica AES
- Encriptar el archivo con la llave simétrica - Encriptar los archivos con la llave simétrica
- Encriptar la llave simétrica con la llave pública dada por el servidor - Encriptar la llave simétrica con la llave pública dada por el servidor
- Enviar el archivo encriptado con el nombre 'data' y la llave encriptada en formato de archivo con el nombre 'key' (data=@data.file, key=@key.file) - Enviar los archivos encriptados con cualquier nombre y la llave encriptada en formato de archivo con el nombre 'key' (somename=@data.file, key=@key.file)
Si no se envía una llave se presume que el archivo no está encriptado. Si no se envía una llave se presume que el archivo no está encriptado.
/data devuelve un string de 16 caracteres que corresponde al mensaje, se usa en el siguiente paso. /data devuelve un id que corresponde al mensaje, se usa en el siguiente paso.
Aclaración: el Content-Type debe ser multipart/form-data Aclaración: el Content-Type debe ser multipart/form-data
...@@ -42,24 +45,38 @@ Aclaración: el Content-Type debe ser multipart/form-data ...@@ -42,24 +45,38 @@ Aclaración: el Content-Type debe ser multipart/form-data
Llamando a este link se envían los parámetros de envío del mensaje pasado en data, y se procede a poner en la cola el mensaje. (POST) Llamando a este link se envían los parámetros de envío del mensaje pasado en data, y se procede a poner en la cola el mensaje. (POST)
- Bajo 'id' se envía el string del paso anterior - Bajo 'id' se envía el id devuelto en el paso anterior
- Bajo 'serv' se envía el servicio a usar, por ahora solo hay 'wpp1' correspondiente a WhatsApp - Bajo 'serv' se envía el servicio a usar, por ahora solo hay 'wpp1' correspondiente a WhatsApp
- Bajo 'dest' se envía el número de destino, el formato puede variar dependiendo del servicio y dicho formato será documentado, el de 'wpp1' es el formato internacional sin el '+' y sin espacios ni guiones - Bajo 'dest' se envía el número de destino, el formato puede variar dependiendo del servicio y dicho formato será documentado, el de 'wpp1' es el formato internacional sin el '+' y sin espacios ni guiones
- Bajo 'type' se envía el tipo de dato a enviar, por ahora solo se permite 'text', los nombres serán documentados. - Bajo 'type' se envía un json { archivo : tipo }, donde a cada archivo se le asigna un tipo
En caso de que no exista el servicio o este no admita el tipo, o que el id no exista, se devolverá un mensaje de error con esa información. Caso contrario, se devolverá el id de base de datos del mensaje para futura consulta. En caso de que no exista el servicio o este no admita el tipo, o que el id no exista, se devolverá un mensaje de error con esa información. Caso contrario, se devolverá el string 'queued' desmotrando que todo salió bien y que el mensaje está en cola.
### myserver.com/cons ### myserver.com/cons
Por este link se consulta el estado de los mensajes que se quieren enviar, mandando el id bajo 'id' (POST). Se devuelve 'queued' o 'delivered'. Si fue enviado el mensaje se archiva y no puede ser consultado nuevamente. Por este link se consulta el estado de los mensajes que se quieren enviar, mandando el id bajo 'id' (POST). Se devuelve 'preprocess', 'queued' o 'delivered'. Si fue enviado el mensaje se archiva y no puede ser consultado nuevamente. 'preprocess' significa que el mensaje no recibió parámetros. 'queued' que no todo fue enviado. 'delivered' que fueron enviados todos los archivos.
### A saber
Los mensajes a los que no se les provea parámetros serán eliminados después de 24 hs. Lo mismo para mensajes que no sean consultados por 24 hs.
## Servicios
WhatsApp 1 - ['wpp1'](/Wpp1.md)
Mail - ['mail'](/Mail.md)
Los tipos que aceptan y la forma de usarlos se encuentran en los links.
## Contribuir
Para contribuir con el proyecto se debe popular el archivo `services.py` # Contribuir
Para contribuir con el proyecto, mayormente, se deberá popular el archivo `services.py`.
La forma de hacer esto consta de lo siguiente: La forma de hacer esto consta de lo siguiente:
- Para servicios existentes se pueden agregar tipos de datos, en cuyo caso deberán ser incluidos en la clase `Datatypes` con su respectivo chequeo de validez en `validate` y se deberá agregar, si se necesita, una nueva restricción de formato (o funcionalidad) para todos los servicios. - Para servicios existentes se pueden agregar tipos de datos, en cuyo caso deberán ser incluidos en la clase `Datatypes` con su respectivo chequeo de validez en `validate` y se deberá agregar, si se necesita, una nueva restricción de formato (o funcionalidad) para todos los servicios.
- Para agregar nuevos servicios se debe hacer una nueva clase que derive de `ServiceBase` e implementar las funciones de dicha clase abstracta. Luego agregar el servicio a `Services` con su validación y expandir `serviceFactory(serv)` para que pueda devolver una instancia de la nueva clase - Para agregar nuevos servicios se debe hacer una nueva clase que derive de `ServiceBase` e implementar las funciones de dicha clase abstracta. Luego agregar el servicio a `Services` y expandir `serviceFactory(serv)` para que pueda devolver una instancia de la nueva clase.
En el método En el método
``` ```
...@@ -70,7 +87,7 @@ En el método ...@@ -70,7 +87,7 @@ En el método
data es un dictionario que contiene los datos del pedido guardados en la base de datos data es un dictionario que contiene los datos del pedido guardados en la base de datos
- En 'id' está el id del pedido, que coincide con el nombre del archivo a mandar - En 'id' está el id del pedido, que coincide con el nombre del archivo a mandar
- En 'dir' está el nombre del directorio bajo el cual se encuentra el archivo a enviar - En 'path' está el nombre del directorio bajo el cual se encuentran los archivos a enviar
- En 'dest' se encuentra el destinatario - En 'dest' se encuentra el destinatario
- En 'type' se encuentra el formato de lo que queremos mandar - En 'type' se encuentra el formato de lo que queremos mandar
- En 'state' se encuentra el estado del mensaje, que siempre será 'queued' y es completamente irrelevante a la operación - En 'state' se encuentra el estado del mensaje, que siempre será 'queued' y es completamente irrelevante a la operación
...@@ -87,10 +104,44 @@ El método ...@@ -87,10 +104,44 @@ El método
toma un string correspondiente al formato de los datos y retorna un booleano correspondiente a si el servicio soporta o no el tipo de dato. Es una buena práctica usar `Datatypes.validate()` para chequear que el formato existe. toma un string correspondiente al formato de los datos y retorna un booleano correspondiente a si el servicio soporta o no el tipo de dato. Es una buena práctica usar `Datatypes.validate()` para chequear que el formato existe.
## Servicios ## Archivos
En `deploy.py` se encuentra el servicio Flask, esta parte se tocará muy poco. En `process.py` se encuentran las aplicaciones de base de datos orientadas a lo que el servicio web necesita. En `database.py` se encuentran las interacciones genéricas con la base de datos. En `services.py` se encuentran las clases que representan los servicios de mensajería. En `enums.py` se encuentran las clases estáticas que facilitan la estructura que maneja el servicio.
## Errores
Si se envía un servicio inexistente, llegará un mensaje de error y el los parámetros serán cancelados. Lo mismo si, dado un servicio, los tipos de los archivos no están soportados o no existen.
Si se envía un tipo de un archivo que no existe se cancelará todo y se retornará el mensaje de error. Lo mismo con el id, aunque sin eso nada se puede hacer, puesto que no se pueden localizar los archivos.
Si en una consulta se envía un id incorrecto se responderá apropiadamente, pero no se va a romper nada.
Errores menos comunes incluyen:
- Intentar usar una tabla de base de datos que no existe
- En DBconnection.update(), usar un comparador no válido
- La data falla los chequeos de validez
## Tests
Hay un archivo que hace los [tests](/test_flask.py) automáticamente si se corre con `python3`. Tarda un minuto para permitir que se envíen todos los mensajes, hay que tener cuidado de que si hace falta que tarde más hay que cambiar la constante de tiempo.
Para agregar más tests, por cada uno debe existir una función y agregar un thread en main.
## Mejoras no implementadas
1. Añadir mas información a los mensajes de mail.
2. Test de mensajes de error.
3. Añadir estado "partially delivered" para mensajes enviados a medias.
4. Cambiar id de /data a string (posiblemente el nombre de la carpeta)*.
5. Posibilidad de usar un emisor de mensajes diferente.
WhatsApp 1 - ['wpp1'](/Wpp1) *Esto, si bien es un golpe duro en performance, porque la búsqueda O(LogN) en base de datos se vuelve O(N), es un incremento sustancial en seguridad, puesto que nadie accidentalmente puede enviar parámetros a mensajes que no son suyos o consultar el estado de un mensaje enviado ajeno y borrarlo, volviéndolo inconsultable.
#### próximamente... ### Soluciones que pensé
Telegram - telegram
\ No newline at end of file \ No newline at end of file
1. Cambiar la tabla "type" a "info" (o simplemente agregar como tabla separada) y que el json acepte parámetros de todo tipo específicos de cada servicio.
4. Remover por completo la noción de id numérico y que el comparador para los updates se vuelva el path
5. Agregar una tbla "sender" y que tenga "default" o algún otro que se prefiera.
\ No newline at end of file \ No newline at end of file
...@@ -4,7 +4,7 @@ Este servicio es capaz de mandar: ...@@ -4,7 +4,7 @@ Este servicio es capaz de mandar:
texto - 'text' texto - 'text'
imagenes - 'image' imagenes - 'image'
todo tipo de archivos - 'media' todo tipo de archivos - 'document'
links - 'link' links - 'link'
Los últimos 3 funcionan de la misma manera, link se usa para cualquier link y los otros dos para su tipo respectivo Los últimos 3 funcionan de la misma manera, link se usa para cualquier link y los otros dos para su tipo respectivo
......
...@@ -5,6 +5,7 @@ class DBconnection: ...@@ -5,6 +5,7 @@ class DBconnection:
tables = ["msg","history"] tables = ["msg","history"]
# Estructura de las tablas
structure = { structure = {
Table.id : "integer PRIMARY KEY", Table.id : "integer PRIMARY KEY",
Table.path : "text", Table.path : "text",
...@@ -20,9 +21,9 @@ class DBconnection: ...@@ -20,9 +21,9 @@ class DBconnection:
for column in DBconnection.structure: for column in DBconnection.structure:
query += column + " " + DBconnection.structure[column] + "," query += column + " " + DBconnection.structure[column] + ","
query = query.strip(",") + ")" query = query.strip(",") + ")"
# main table # Tabla principal
self.query(query) self.query(query)
# delivered messages that were informed # Historial de mensajes enviados que fueron consultados
self.query(query.replace("msg","history",1)) self.query(query.replace("msg","history",1))
def query(self,query,*args): def query(self,query,*args):
......
...@@ -7,21 +7,25 @@ from enums import States, Table ...@@ -7,21 +7,25 @@ from enums import States, Table
import os, ipdb, time, threading, random, datetime import os, ipdb, time, threading, random, datetime
app = Flask(__name__) app = Flask(__name__)
# carpeta de los mensajes
msgfolder = "msg/"
# tiempo para reintentar mensajhes
retry_timer = 10 retry_timer = 10
# tiempo para limpieza
clean_timer = 20 clean_timer = 20
# largo de los nombres de las carpetas bajo msg/
prefix_lenght = 16 prefix_lenght = 16
# tiempo límite que debe pasar para eliminar cosas
operation_timer = 86400 operation_timer = 86400
# conección a base de datos
# folder for all messages
msgfolder = "msg/"
# database connection
process = Process("messages.db") process = Process("messages.db")
@app.route('/') @app.route('/')
def main(): def main():
return render_template('index.html') return render_template('index.html')
# Devuelve la llave pública del server
@app.route('/key', methods = ['GET', 'POST']) @app.route('/key', methods = ['GET', 'POST'])
def key(): def key():
f = open("rsa_key.pub", "r") f = open("rsa_key.pub", "r")
...@@ -29,6 +33,8 @@ def key(): ...@@ -29,6 +33,8 @@ def key():
f.close() f.close()
return key return key
# Guarda archivos del emisor en una nueva carpeta (desencriptados)
# Pide guardar en base de datos una preinstancia del mensaje
@app.route('/data', methods = ['POST']) @app.route('/data', methods = ['POST'])
def data(): def data():
prefix = newprefix() prefix = newprefix()
...@@ -38,15 +44,15 @@ def data(): ...@@ -38,15 +44,15 @@ def data():
key = request.files.get('key') key = request.files.get('key')
if key: if key:
request.files[file].save(path + "rand.key.enc") request.files[file].save(path + "rand.key.enc")
# decrypt random key with stored private key and store in prefix folder # denecripto llave simétrica con mi llave privada y la guardo en la carpeta del emisor
os.system("openssl rsautl -decrypt -inkey rsa_key.pri -in " + path + "rand.key.enc -out " + path + "rand.key") os.system("openssl rsautl -decrypt -inkey rsa_key.pri -in " + path + "rand.key.enc -out " + path + "rand.key")
os.remove(path + "rand.key.enc") os.remove(path + "rand.key.enc")
for file in request.files: for file in request.files:
# if key exists and this is not it # si hay llave y no es esta
if key and file != "key": if key and file != "key":
request.files[file].save(filepath + ".enc") request.files[file].save(filepath + ".enc")
# decrypt file with decrypted random key and store in prefix folder # desencripto archivo con la llave simétrica y lo guardo en la carpeta del emisor
os.system("openssl enc -d -aes-256-cbc -in " + filepath + ".enc -out " + filepath + " -pass file:" + path + "rand.key") os.system("openssl enc -d -aes-256-cbc -in " + filepath + ".enc -out " + filepath + " -pass file:" + path + "rand.key")
os.remove(filepath + ".enc") os.remove(filepath + ".enc")
else: else:
...@@ -57,6 +63,7 @@ def data(): ...@@ -57,6 +63,7 @@ def data():
return str(process.datastore(path)) return str(process.datastore(path))
# Devuelve un string al azar entre [0-9,a-z,A-Z]
def newprefix(): def newprefix():
prefix = "" prefix = ""
i = 0 i = 0
...@@ -71,6 +78,7 @@ def newprefix(): ...@@ -71,6 +78,7 @@ def newprefix():
i += 1 i += 1
return prefix return prefix
# Guarda los parámetros de los archivos enviados en /data
@app.route('/msg', methods = ['POST']) @app.route('/msg', methods = ['POST'])
def msg(): def msg():
id = request.values['id'] id = request.values['id']
...@@ -83,21 +91,24 @@ def msg(): ...@@ -83,21 +91,24 @@ def msg():
state = process.paramstore(query) state = process.paramstore(query)
return state return state
# Consulta el estado de un mensaje dado su id
@app.route('/cons', methods = ['POST']) @app.route('/cons', methods = ['POST'])
def cons(): def cons():
id_query = request.values['id'] id_query = request.values['id']
row = process.lookup(id_query) row = process.lookup(id_query)
if type(row) == str: # error message if type(row) == str: # error
return row return row
if row[Table.state] == States.delivered: if row[Table.state] == States.delivered:
os.system("rm -r " + row[Table.path]) os.system("rm -r " + row[Table.path])
return str(row[Table.state]) return str(row[Table.state])
# Intenta enviar mensajes
def attempt(): def attempt():
p = Process('messages.db') p = Process('messages.db')
p.send() p.send()
threading.Timer(retry_timer, attempt).start() threading.Timer(retry_timer, attempt).start()
# Limpia basura
def clean(): def clean():
p = Process('messages.db') p = Process('messages.db')
paths = p.paths() paths = p.paths()
...@@ -105,21 +116,22 @@ def clean(): ...@@ -105,21 +116,22 @@ def clean():
for folder in paths: for folder in paths:
mtime = os.path.getmtime(folder) mtime = os.path.getmtime(folder)
# if the folder exists for more than a X seconds, erase it and its contents # Si la carpeta existe por más de X segundos, borrala
if int(now.strftime("%Y%m%d%H%M%S")) - int(time.strftime("%Y%m%d%H%M%S")) > operation_timer: if int(now.strftime("%Y%m%d%H%M%S")) - int(time.strftime("%Y%m%d%H%M%S")) > operation_timer:
os.system("rm -r " + folder) os.system("rm -r " + folder)
threading.Timer(clean_timer, clean).start() threading.Timer(clean_timer, clean).start()
# Inicio del servicio
if __name__ == "__main__": if __name__ == "__main__":
# generate keys # Genero llaves
os.system("openssl genrsa -out rsa_key.pri 4096") # private key os.system("openssl genrsa -out rsa_key.pri 4096") # private key
os.system("openssl rsa -in rsa_key.pri -out rsa_key.pub -outform PEM -pubout") # public key os.system("openssl rsa -in rsa_key.pri -out rsa_key.pub -outform PEM -pubout") # public key
# starts attempt daemon # Demonio attempt
attempt() attempt()
# starts cleaning daemon # Demonio clean
clean() clean()
# remove and recreate msg folder for cleanup purposes # Borro y creo msg/ por limpieza
if os.path.exists(msgfolder): if os.path.exists(msgfolder):
os.system("rm -r " + msgfolder) os.system("rm -r " + msgfolder)
os.mkdir(msgfolder) os.mkdir(msgfolder)
......
...@@ -4,7 +4,7 @@ URL = "http://192.168.15.75:5000/" ...@@ -4,7 +4,7 @@ URL = "http://192.168.15.75:5000/"
TESTPHONE = "5493415959169" TESTPHONE = "5493415959169"
TESTMAIL = "sujetodeprueba0110@gmail.com" TESTMAIL = "sujetodeprueba0110@gmail.com"
FOLDER = "testfolder/" FOLDER = "testfolder/"
SENDING_TIME = 40 # aumentar a medida que se vuelva necesario SENDING_TIME = 60 # aumentar a medida que se vuelva necesario
def wpp1(phone): def wpp1(phone):
id = requests.post(url = URL + "data", files = { id = requests.post(url = URL + "data", files = {
...@@ -56,6 +56,37 @@ def mail(mail): ...@@ -56,6 +56,37 @@ def mail(mail):
}) })
assert (state.text == "delivered") , "'" + state.text + "' no es igual a 'delivered'" assert (state.text == "delivered") , "'" + state.text + "' no es igual a 'delivered'"
def encryption(phone):
key = requests.get(url = URL + "key")
f = open(FOLDER + "server_rsa_key.pub", "+w")
f.write(key.text)
f.close()
os.system("openssl rand -base64 32 > " + FOLDER + "rand.key")
os.system("openssl enc -aes-256-cbc -salt -in " + FOLDER + "plaintext" " -out " + FOLDER + "plaintext.enc -pass file:" + FOLDER + "rand.key")
os.system("openssl enc -aes-256-cbc -salt -in " + FOLDER + "plainimage" " -out " + FOLDER + "plainimage.enc -pass file:" + FOLDER + "rand.key")
os.system("openssl rsautl -encrypt -inkey " + FOLDER + "server_rsa_key.pub -pubin -in " + FOLDER + "rand.key -out " + FOLDER + "rand.key.enc")
id = requests.post(url = URL + "data", files = {
'plaintext.enc' : open(FOLDER + "plaintext.enc","rb"),
'plainimage.enc' : open(FOLDER + "plainimage.enc","rb")
})
assert int(id.text) > 0 , id.text + "no es mayor a 0"
state = requests.post(url = URL + "msg", params = {
'id' : id.text,
'serv' : "wpp1",
'dest' : phone,
'type' : json.dumps({
'plaintext' : 'text',
'plainimage' : 'image'
})
})
assert state.text == "queued" , "'" + state.text + "' no es igual a 'queued'"
time.sleep(SENDING_TIME)
state = requests.post(url = URL + "cons", params = {
'id' : id.text
})
assert (state.text == "delivered") , "'" + state.text + "' no es igual a 'delivered'"
def errors(): def errors():
# service not valid # service not valid
# service unable to send type # service unable to send type
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!