Introducción - UltraTesla/UTesla GitHub Wiki
En Ultra Tesla existen los servicios, que son básicamente pequeños programas que proporcionan objetivos específicos, quizá para usuarios específicos. La ruta de los servicios pueden tener una sintaxis parecida a: <Dirección del servidor>:[<Puerto>]/<Servicio>[/<Sub servicio>][/<...>].
Puede haber tantos sub servicios como el administrador del servidor lo haya propuesto, pero la diferencia sutil entre un servicio y un sub servicio, es que el primero no es más que un identificador para las redes de confianza (o nodos).
Un ejemplo real podría ser crear un servicio llamado "hello_world" y éste podría estar en una ruta real como la siguiente: localhost:17000/hello_world
Para poder crear un servicio como el anteriormente mencionado, necesitamos dirigirnos a la carpeta "services" y crear una nueva carpeta llamada según el nombre de nuestro servicio:
cd services
mkdir hello_world
Ahora para que el núcleo pueda detectarlo, es necesario crear un archivo en esa carpeta llamado igual que ella, pero con la extensión de python: .py
hello_world/hello_world.py
class Handler:
@property
def SUPPORTED_METHODS(self):
return "show"
def SET_CONTROLLER(self, controller, /):
self.controller = controller
async def show(self):
await self.controller.write("Hello World!")
Ahora un cliente podría requerir ese servicio, por lo que un cliente se vería de la siguiente manera:
client.py
import asyncio
from modules.Infrastructure import client
async def main():
(host, port) = "localhost", 17000
user = "<Nombre de usuario>"
server_key = "<Clave pública del servidor>"
public_key, private_key = "<Clave pública del usuario>", "<Clave privada del usuario>"
with open(server_key, "rb") as fd:
server_key = fd.read()
with open(public_key, "rb") as fd:
public_key = fd.read()
with open(private_key, "rb") as fd:
private_key = fd.read()
(UControl, _, stream) = await client.simple_client(
host,
port,
user,
server_key,
public_key = public_key,
private_key = private_key,
)
UControl.set_token("<Token de acceso>")
UControl.set_path("/hello_world", "show")
await UControl.write(None)
print(await UControl.read())
stream.close()
if __name__ == "__main__":
asyncio.run(main())
Al ejecutar deberíamos tener la siguiente respuesta:
python3.8 client.py
Hello World!
El administrador será el encargado de crear los usuarios, aunque perfectamente se podría automatizar como un sistema de registro cualquiera con una interfaz de usuario cualquiera, para este ejemplo se usará el plugin 'add' de UTeslaCLI.
# Para requerir ayuda
python3.8 ./UTeslaCLI add --help
# Creamos el usuario
python3.8 ./UTeslaCLI add -u <Nombre de usuario> -p <Contraseña> -i <Clave pública del usuario>
Nota: El nombre de usuario es único por cada usuario a registrar
Tanto el servidor como el usuario deben tener sus par de claves para que la comunicación sea satisfactoria. Esto es un proceso inicial que deben hacer antes que nada (independientemente de si es un usuario o el administrador del servidor).
El cliente va a requerir usar el plugin 'generate_keys' para la creación del par de claves
python3.8 ./UTeslaCLI generate_keys -out-public-key <Nombre del archivo de la clave pública> -out-private-key <Nombre del archivo de la clave privada>
Eso generaría el par de claves Ed25519 que se usará para firmar y verificar datos en plena comunicación.
Ahora lo que tendría que hacer el usuario que desea registrarse es enviar la clave pública al administrador del servidor para que éste lo registre satisfactoriamente.
El proceso para generar el par de claves en el lado del servidor será mucho más simple; se hará automáticamente cuando se ejecute.
python3.8 ./UTesla
Este proceso se puede hacer por distintos medios, sin embargo hay plugins interesantes que podrían facilitar esto y ahorrar un montón de tiempo y esfuerzo. Como lo pueden ser el plugin shareKeys y getKey.
En el caso de querer compartir la clave pública:
python3.8 ./UTeslaCLI shareKeys <Ruta de la clave pública a compartir>
Un ejemplo realista podría ser:
python3.8 ./UTeslaCLI shareKeys /tmp/pubkey
También se pueden usar algunas "constantes" para indicar algunas claves, como pueden ser la de los nodos (o servidores de confianza), la de los usuarios registrados o la del mismísimo servidor.
Para compartir la clave pública del servidor:
python3.8 ./UTeslaCLI shareKeys server
Para compartir las claves públicas de los nodos:
python3.8 ./UTeslaCLI shareKeys nodes
Para compartir las claves públicas de los usuarios registrados:
python3.8 ./UTeslaCLI shareKeys users
Nota: Si hay un archivo en la ruta actual con el mismo nombre que las constantes se tiene en cuenta y no éstas, por lo que habría que tener cuidado con eso.
Ahora un usuario o un administrador del servidor podría obtener la clave pública de alguna parte deseada usando el plugin getKey:
python3.8 ./UTeslaCLI getKey -o /tmp localhost/pubkey
La salida podría ser algo como:
La huella de la clave es SHA3_256:bc42b04937ce6494c43ee5134cb5872f2cefed48b55c5b5c977a1e25d68c2697
¿Deseas continuar con la operación (sí/no)? sí
Guardada: /tmp/pubkey
Antes de hacer nada las dos partes deben saber la huella de clave para poder saber que no se modificó en la transferencia y evitar un posible ataque contra la infraestructura.
Se puede usar el siguiente comando para saber exactamente la huella de la clave a compartir:
echo -n <Ruta de la clave pública> | python3.8 -c "import hashlib, sys; print(hashlib.sha3_256(open(sys.stdin.read().strip(), 'rb').read(32)).hexdigest())"
Ahora la salida debe ser compartida con la otra parte y toda esta operación la deben hacer las dos partes para poder comunicarse con éxito.
UTesla requiere de un token de acceso para realizar la mayoría de acciones, en este caso el cliente deseoso por obtener su token tiene que usar el plugin generate_token y el servidor debe tener el servicio generate_token.
python3.8 ./UTeslaCLI generate_token -s <La clave pública del servidor> -i <La clave pública del usuario> -I <La clave privada del usuario> -u <El nombre de usuario> -p <La contraseña del usuario> <Dirección del servidor>
Supongamos que en el mundo haya dos servidores y los administradores se conozcan, uno de éstos contiene uno o más servicios que otro no, pero un cliente que requiera el uso de ese servicio usando la dirección del primer servidor (por ejemplo) no tendría ningún inconveniente, ya que si el servicio deseado no se encuentra en el primer servidor, éste hará una búsqueda en la red y determinará qué servidor es el que contiene ese servicio, posteriormente obtendría una respuesta como si fuera el mismo cliente y luego la redirige hacia el cliente mismo, por lo que todo sería transparente.
Una perfecta explicación gráfica podría ser:
Nota: El esquema se generó usando el plugin gen_onodo
En este caso el servidor local () tiene los siguientes servicios de forma local: admin, generate_token, index, get_services; no obstante a pesar de eso, un cliente no lo vería así, sino que éste creería que el servidor tiene los siguientes servicios: admin, generate_token, index, get_services, cloud, torrent, music_player, hello_world y calculator.
Todos los servicios de las demás redes ahora serán parte del servidor local, y si un usuario deseara usar uno de esos servicios, ¡puede hacerlo! a pesar de que físicamente no estén allí.
Lo anterior es muy útil si se desea separar ciertos servicios entre servidores, por cuestiones económicas, de recursos, una mejoría en la implementación o cualquier cosa que se desee, al fin y al cabo, el usuario es el que lo decide. Un administrador que busca una solución por software podría usar un servidor con menos capacidad de procesamiento pero con más capacidad de almacenamiento para guardar archivos de gran magnitud y podría usar el servidor local para el procesamiento, todo en una misma interfaz para un mismo cliente.
Como ejemplo, vamos a usar el servicio hello_world en otro servidor y nosotros los clientes deberíamos obtener una respuesta como si nada hubiera ocurrido o cambiado.
hello_world/hello_world.py
class Handler:
@property
def SUPPORTED_METHODS(self):
return ("show", "show1")
def SET_CONTROLLER(self, controller, /):
self.controller = controller
async def show(self):
await self.controller.write("Hello World!")
async def show1(self):
await self.controller.write("Hello!")
El cliente que usaremos será el siguiente:
import asyncio
from modules.Infrastructure import client
async def main():
(host, port) = "localhost", 17000
user = "<Nombre de usuario>"
server_key = "<Clave pública del servidor>"
public_key, private_key = "<Clave pública del usuario>", "<Clave privada del usuario>"
with open(server_key, "rb") as fd:
server_key = fd.read()
with open(public_key, "rb") as fd:
public_key = fd.read()
with open(private_key, "rb") as fd:
private_key = fd.read()
(UControl, _, stream) = await client.simple_client(
host,
port,
user,
server_key,
public_key = public_key,
private_key = private_key,
)
UControl.set_token("<Token de acceso>")
UControl.set_path("/hello_world", "show")
await UControl.write(None)
print(await UControl.read())
stream.close()
if __name__ == "__main__":
asyncio.run(main())
Como se puede observar, es exactamente igual, no hay absolutamente ningún cambio en el cliente.
El esquema quedaría de la siguiente forma:
Como se puede observar, el servicio se encuentra en la otra red, pero si un usuario quisiera conectarse al servidor local y usar ese servicio obtendría:
python3.8 client.py
Hello World!
Como se puede apreciar, es un proceso muy sencillo y lo mejor, es transparente para el usuario.