En esta práctica realizaremos un sistema de autentificación de usuarios que formará parte de un sistema mayor en las próximas prácticas usando streams. La idea principal es establecer un protocolo que entenderá cualquiera de los programas de cualquiera de los grupos de modo que puedan comunicarse entre sí.
Está permitido comentar y debatir la práctica con otros grupos, incluso se recomienda probar el protocolo con otros grupos, pero bajo ningún concepto está permitido compartir o copiar código de otros grupos. Incumplir esta norma acarreará el suspenso de la práctica.
El protocolo a nivel de aplicación será de tipo cliente-servidor. La aplicación cliente establecerá una conexión TCP con un servidor. Una vez que el cliente establece la conexión con el servidor, este último no enviará nada por iniciativa propia, tan sólo responderá a interacciones iniciadas desde el cliente. Una interaccion sin embargo puede incluir un intercambio de más de una pregunta y respuesta.
La comunicación una vez iniciada la conexion consistira en el envio de lineas de texto en ASCII (es decir, no vamos a transmitir información en formato binario), vamos a utilizar streams tal y como se ha visto en la clase de teoría. Esto quiere decir que no debemos emplear las funciones de bajo nivel read, write, recv, send sino fdopen, fgets, fprintf, sscanf, etc.
Cada linea será una secuencia de caracteres ASCII cuyo final es el salto de línea (“\n”). Este salto de línea no forma parte del mensaje, es lo que separa un mensaje de otro cuando llega información por el socket.
| M | E | N | S | A | J | E | 1 | ‘\n’ | M | E | N | S | A | J | E | 2 | ‘\n’ |
Es importante recordar que en C no existen “strings” sino que se utiliza un array de char que contiene un carácter en cada posición del array. Para indicar el final del “string” se usa un byte en el que todos son bits son cero (es decir, el entero 0 o también el ‘\0’ escrito en C).
El protocolo consta de 3 fases. Al establecer la conexión esta en el estado INICIADO, una vez que el cliente inicia el intento de autenticacion estamos en el estado de LOGIN y cuando esté acaba correctamente estará en el estado de AUTENTICADO.
Las transiciones de los estados seguirán el siguiente diagrama:

Cualquier mensaje recibido por parte del cliente que no se haga en el momento correcto (por ejemplo un SETNAME antes de realizar un LOGIN), provocará que el servidor envíe el mensaje ERROR y cierre la conexión directamente.
Con la conexion establecida estamos en el estado inciado. El cliente puede iniciar las siguientes interacciones
| Cliente | dirección | Servidor |
|---|---|---|
| INFO | -> | |
| <- | SERVER … |
El servidor responderá con una cadena con el siguiente formato
c2s: INFO
s2c: SERVER nombredelservidor tiempounix informacion
Ejemplo:
c2s: INFO
s2c: SERVER Servidor-de-prueba 1708160547 v0.1
El cliente puede iniciar el proceso de autenticacion que sigue este proceso
| Cliente | dirección | Servidor |
|---|---|---|
| LOGIN [id] | -> | |
| <- | RETO [nonce] | |
| RESPUESTA [ri1] | -> | |
| <- | RETO [i2] 2 | |
| RESPUESTA [ri2] | -> | |
| <- | RETO [i3] 1 | |
| RESPUESTA [ri3] | -> | |
| <- | LOGIN OK |
| Cliente | dirección | Servidor |
|---|---|---|
| LOGIN [id] | -> | |
| <- | RETO [i1] 3 | |
| RESPUESTA [ri1] | -> | |
| <- | RETO [i2] 2 | |
| RESPUESTA [ri2] | -> | |
| <- | LOGIN FAIL |
El cliente envia un login indicando su identidad. La identidad o nombre de usuario o userid sera una cadena que tendra almacenada el servidor para cada uno de los usuarios que tenga definidos. Cada identidad consistira en una cadena de identificador de usuario unica en dicho servidor y una cadena de secreto o contraseña que solo sera conocida por el usuario para demostrar su identidad.
c2s: LOGIN userid
El servidor respondera a LOGIN enviando al cliente una serie de retos, cada reto tiene este formato
s2c: RETO x [i]
x es un numero entero es la posicion de una letra del secreto empezando en 0
i es un número de información opcional, el servidor puede enviar cuanos retos quedan o cual es el numero de reto o incluso no enviar nada
Por cada reto el cliente debe responder indicando el caracter i del secreto. Si i es mayor que la longitud del secreto se supondra que el secreto se repite indefinidamente. Los secretos deben tener al menos 10 caracteres.
Por ejemplo si un usuario tiene de nombre mikel y secerto rcPASS4M1k3l un ejemplo de login podria ser
0123456789ab
rcPASS4M1k3l
c2s: LOGIN mikel
s2c: RETO 4 5 ## la letra 4 es S
c2s: RESPUESTA S
s2c: RETO 10 4 ## la letra 10 es 3
c2s: RESPUESTA 3
s2c: RETO 31 3 ## 31%12 es 7 la letra 7 es M
c2s: RESPUESTA M
s2c: RETO 2 2 ## la letra 2 es P
c2s: RESPUESTA P
s2c: RETO 12321 1 ## 12321%12 es 9 la letra 9 es k
c2s: RESPUESTA k
s2c: LOGIN OK
El cliente no sabe a priori cuantos RETOS va a preguntar el servidor. Debe contestar hasta que le digan LOGIN OK o ERROR
El servidor debe elgir las preguntas al azar. No es necesario que no repita las preguntas. En el servidor se podrá configurar cuantas preguntas hace. Si la identidad no esta el servidor puede enviar ERROR directamente aunque es más apropiado que envie un reto que vaya a fallar para no dar pistas.
Durante la fase de LOGIN el servidor solo aceptara mensajes RESPUESTA, todos los demas mensajes incluso aunque sean validos como INFO dispararan un ERROR y que se cierre la conexión.
Este protocolo de autenticación se usa con fines didácticos para programar, esta claro que tiene fallos de seguridad importantes.
Tras recibir o enviar el LOGIN OK, cliente y servidor estan en estado autenticado.
Una vez el cliente está correctamente autentificado, podrá cambiar su nombre y consultar su valor actual. En esta fase también se aceptará el comando INFO. En futuras prácticas se añadiran más operaciones que pueden hacerse una vez autenticado.
Aparte del identificador de usuario una vez establecida una sesión autenticada se puede elegir un nombre para la sesión. En caso se usarse para un juego o chat este nombre es el que verán el resto de los usuarios.
En caso de querer cambiar su nombre:
| Cliente | dirección | Servidor |
|---|---|---|
| SETNAME [nombre] | -> |
Donde [nombre] es el nombre elegido por el cliente. Estará compuesto por una única palabra de hasta 16 caracteres compuesta por letras mayúsculas, minúsculas y números.
si el nombre se ha cambiado correctamente:
| Cliente | dirección | Servidor |
|---|---|---|
| <- | SETNAME OK |
Si ha existido cualquier error y no se ha procedido a cambiar el nombre. Por ejemplo un sistema podría no aceptar que varias conexiónes tengan el mismo nombre y ese ya estar asignado a otro.
| Cliente | dirección | Servidor |
|---|---|---|
| <- | SETNAME ERROR |
En ambas respuestas, la conexión permanecerá abierta.
En caso de que el cliente quiera conocer el nombre que tiene asignado:
| Cliente | dirección | Servidor |
|---|---|---|
| GETNAME | -> | |
| <- | GETNAME [nombre] |
Si el cliente no ha definido ningún nombre, el valor por defecto es “Invitado”.
Realizar un cliente que cumpla el protocolo.
Esta es la especificación del cliente:
FORMATO
cliente <ip> <puerto> <userid> [<secreto>]
DESCRIPCIÓN
<ip> IP en formato IPv4, por ejemplo, 10.1.1.101
<puerto> puerto al que conectarse, por ejemplo 6000
<userid> userid para emplear en la autenticación
<secreto> secreto para la autenticación
El userid debe proporcionarse en la llamada a cliente y puede proporcionarse tambien el secreto. Si se proporcionan los dos el cliente utilizara ese <userid> y <secreto> para autentificarse.
Si el secreto no se proporciona se le preguntara al usuario.
Bajo ningun concepto el cliente cargará el secreto de un fichero y especialmente es un error que el cliente abra y cargue datos del fichero usuarios del servidor.
El cliente se autenticará con el usuario indicado y permitirá cambiar y consultar el nombre.
Seguidamente se muestra un ejemplo de ejecución. El interfaz no es importante, puedes elegir la forma que prefieras, pero debe funcionar con el protocolo dado.
Nótese que el usuario o bien introduce el secreto en la linea de comando o bien se le pregunta el secreto una sóla vez y es el programa el que gestiona el protocolo de contestar a la letra correcta cada vez. Es un error pedir al usuario que resuelva los retos.
$ ./cliente 10.1.1.101 6000 mikel rcPASS4M1k3l
Autenticando con usario mikel
reto 1 respondido
reto 2 respondido
reto 3 respondido
Autenticado OK
1- consultar nombre
2- cambiar nombre
3- cerrar conexion
$ ./cliente 10.1.1.101 6000 mikel
Indique el secreto del usuario mikel: ********
Autenticando con usario mikel
reto 1 respondido
reto 2 respondido
reto 3 respondido
Autenticado OK
1- consultar nombre
2- cambiar nombre
3- cerrar conexion
1
Consultando nombre: invitado
1- consultar nombre
2- cambiar nombre
3- cerrar conexion
2
Que nombre quieres? : Neo
Cambiando nombre OK
Consultando nombre: Neo
1- consultar nombre
2- cambiar nombre
3- cerrar conexion
Envie un makefile al repositorio que compile el cliente
Realizar un servidor que cumpla el protocolo.
Esta es la especificación del servidor es:
FORMATO
servidor <puerto> <userfile> <nretos>
DESCRIPCIÓN
<puerto> puerto en el que escuchar, por ejemplo 6000
<userfile> fichero con usuarios para autenticar
<nretos> numero de retos que exigira para autenticar (si no esta presente el servidor puede elegir los que quiera el autor)
userfile será un fichero con lineas separadas por espacios con los siguientes campos
userid secreto privilegios y si quiere mas campos
un ejemplo de userfile
admin Adminp4ssw0rd546123 10
prueba pruebadesecreto 0
mikel rcPASS4M1k3l 0
El servidor mostrará por pantalla información relevante del proceso, aunque el objetivo es que cumpla con el protocolo. Al igual que con el cliente, aquí tienes un ejemplo de cómo podría ser la ejecución del servidor basándonos en el ejemplo anterior:
$ ./servidor 6000 usuarios
Escuchando conexiones en el puerto 6000
Conexión establecida desde la IP 10.1.1.123 puerto 32125
Recibida petición de INFO
Recibido peticion de LOGIN de mikel
Enviando reto 1
Respuesta ok
Enviando reto 2
Respuesta fallo
Cerrando conexion
Recibido peticion de LOGIN de mikel
Enviando reto 1
Respuesta ok
Enviando reto 2
Respuesta ok
Enviando reto 3
Respuesta ok
Autenticado
Recibida peticion de GETNAME enviando
Recibida peticion de SETNAME cambiando
Recibida peticion de GETNAME enviando
Conexion cerrada por el cliente
...
El servidor no tiene que ser capaz de atender a varios cliente a la vez (eso es la siguiente práctica), solo uno detras de otro. Pero si que tiene que quedarse indefinidamente atendiento clientes al terminar.
Envie un makefile al repositorio que compile el servidor
En el mismo directorio practicas que creamos en la práctica anterior, crear un directorio que se llame practica3. En practica3 existirán al menos estos ficheros:
makefilecliente.cservidor.cEl fichero makefile tendrá cuatro objetivos: todos, cliente, servidor y clean.
todos compilará tanto cliente como servidor.clean será capaz de borrar los ejecutables cliente y servidor. Muy importante: no borrar las fuentes cliente.c y servidor.c
Los ficheros que se entregan se recogerán directamente de Bitbucket. No se admite otro método de entrega.
No entregar los archivos de construcción intermedios ni los ejecutables.