Aplicaciones tipo Telnet con PHP

Telnet en Windows XP

El término Telnet proviene del acrónimo Telecommunication Network para designar un protocolo de red que también forma parte de los protocolos TCP/IP, los que sirven para la comunicación en Internet. La necesidad de usar esta herramienta se originó cuando estaba estudiando el tema de la seguridad en las sesiones PHP. La comunicación en Internet se basa en protocolos, algo que tendré que estudiar más a fondo. Pero por ahora me interesaba conocer hasta que punto es posible establecer esa comunicación sin contar con un navegador.

En Windows el Cliente Telnet es una aplicación que utiliza el protocolo del mismo nombre Telnet. Podemos decir que esa aplicación en nuestro Windows hace de navegador y se conecta a un servidor que recibe las órdenes expresadas según protocolos de TCP/IP. En estos ejemplos usaremos además HTTP para establecer la comunicación con el servidor.

Sin entrar en más detalles, veámos un ejemplo de ejecución. En primer lugar vamos a INICIO y ponemos telnet en el cuadro para ejecutar el programa telnet.exe:

telnet-1

Seguidamente nos sale una pantallas MSDOS como ésta:

telnet-2

A continuación conectamos con el servidor, que en nuestro caso será localhost. En un caso con servidor real sería al go como www.wextensible.com o incluso su IP si la conocemos. Este comando se compone de open más dominio más el número de puerto donde está escuchando el servidor, normalmente el puerto 80. En definitiva ponemos open localhost 80 y pulsamos enter:

telnet-1

Seguidamente la pantalla se queda con Conectándose a localhost.... Uno esperaría un mensaje como "conectado" o similar. Sin embargo no es esto lo que sucede, aunque observando la barra del título, aparece Telnet localhost como indicativo de que se consiguió la conexión.

telnet-2

En cualquier caso si no aparece ningún mensaje de error y el título nos dice eso, a continuación tecleamos CTRL+ (control y signo +) y la pantalla vuelve a Microsoft Telnet >:

telnet-1

Por último damos a enter y entramos en una pantalla totalmente en negro, con el cursor preparado para interaccionar con el servidor localhost:

telnet-2

Es aquí donde entra en juego el protocolo HTTP. Uno de sus métodos es GET para solicitar algo al servidor. Si queremos solicitar la página inicial del dominio, hemos de poner:

GET / HTTP/1.1
Host: localhost

Hay que tener en cuenta que al teclear en esta consola no podemos usar las teclas de retroceso si nos equivocamos en un caracter. En las pruebas que hice observé que había que introducir los caracteres sin equivocaciones, pues las teclas de retroceso son también enviadas al servidor y luego nos responde con algún tipo de error. En todo caso lo que hay que poner es una primera línea con GET / HTTP/1.1 pulsando enter al final. Luego una segunda línea con Host: localhost y pulsar enter DOS VECES. La primera barra después de GET realmente está accediendo al documento por defecto del sitio: index.html, default.html o el que sea que esté por defecto. En definitiva ahí se pondría la ruta hasta el archivo que queremos abrir. Si sólo se pone la barra, se entiende el documento index del sitio. Al final veremos la página inicial de la que mostramos aquí sólo una parte:

telnet

Usar estas aplicaciones nos permite descubrir las cabeceras que envían los servidores. Es obvio que para entenderlas totalmente hay que estudiarse a fondo los protocolos como el HTTP, pero por ahora nos dan una perspectiva diferente que la del simple uso de un navegador.

Como dije antes, si usamos la tecla de retroceso para corregir las equivocaciones cuando estamos tecleando, esa tecla también se envía. Para evitarlo podemos copiar todo lo que vamos a teclear en algún Notepad y luego copiarlo. Entonces en las pantalla del Telnet vamos a la barra superior, en el icono de la iquierda y buscamos la opción de menú editar y pegar:

telnet

A continuación se muestra un ejemplo ejecutado en mi localhost con este formulario que envía un campo a una página PHP llamada action-telnet.php. Se trata de exponer como viajan los parámetros de un formulario enviados con el método GET en una URL.

Ejemplo:

Formulario con method="get"

Este script tiene el siguiente código, con el color marrón para el PHP y el azul para el HTML:

<?php
//Iniciamos las variables que contendrán los valores enviados por el usuario
$un_campo = "";

//Iniciamos recogiendo los GET enviados por el usuario
foreach($_GET as $campo=>$valor){
    switch ($campo) {
        case "un-campo":
            //La función htmlspecialchars() evita que
            //el usuario envíe caracteres no deseados
            $un_campo = htmlspecialchars($valor);           
            break;
                    
                        
    }
} 
//Ahora componemos la página de salida
?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
<head>
    <title>Respuesta</title>
    <meta http-equiv="X-UA-Compatible" content="IE=8" />    
    <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
    <meta name="author" content="Andrés de la Paz" />
    <meta name="copyright" content="© 2010" />    
</head>
<body>
    ...
    <h3>RESPUESTA DESDE EL SERVIDOR AL USUARIO CON PHP</h3>
    <p>El usuario nos ha enviado los siguientes datos:</p>
    <ul>
        <li>Campo: <?php echo $un_campo; ?></li>
                
    </ul>

</body>
</html>    
    

Ese documento action-telnet.php está en la ruta relativa /herramientas/telnet-php/action-telnet.php. Luego acompañamos a la ruta ?un-campo=1234 de tal forma que estamos enviando un valor de formulario 1234 que será recogido por el script en el control un-campo. Recuerde que podemos escribir toda la petición en un Notepad y luego pegarla en el Telnet de Windows XP con el menú que está en la barra superior, en el icono de la izquierda:

GET /herramientas/telnet-php/action-telnet.php?un-campo=1234 HTTP/1.1
Host: localhost
⇒ 

El símbolo de la flecha sólo indica que hay una línea en blanco con objeto de poder presentar en pantalla lo que vamos a enviar antes de poner la siguiente línea:

telnet-1

Ahora pulsamos la tecla enter otra vez para adjuntar la segunda línea en blanco y el servidor nos devuelve la página action-1.php con el valor recibido un-campo=1234:

telnet-2

Se observa como podemos enviar un campo de un formulario sin ni siquiera usar ninguna página web en un navegador. Por lo tanto queda claro que la comunicación con un servidor no empieza ni acaba en el navegador. Por fuera hay un montón de aplicaciones que también pueden interaccionar con nuestro sitio, a veces por desgracia para mal.

Telnet con PHP

En el manual PHP, en el tema sobre la función fsockopen() vemos que nos permite abrir una conexión con un dominio de Internet y así también nos sirve para establecer esa comunicación cliente como antes hicimos con el Telnet de Windows. El manual expone la declaración de esa función así:

resource fsockopen(
    string $hostname [,
    int $port = -1 [,
    int &$errno [,
    string &$errstr [,
    float $timeout = ini_get("default_socket_timeout") ]]]]
    )

El $hostname es el nombre del dominio donde queremos solicitar una petición de una página web. El puerto $port es el 80 para el protocolo HTTP. Los argumentos $errno, $errstr nos devuelven el número y el mensaje de error en su caso. El último argumento es la espera máxima del servidor para establecer la conexión antes de abortar el intento.

Esta función abre un socket que se comporta como un manejador de fichero, donde podemos escribir con fwrite() que equivale a enviar una petición y, por otro lado, leer líneas con fgets(), que equivale a recibir una petición. En el citado enlace de PHP puede encontrar un ejemplo básico que reproducimos literalmente aquí:

<?php
$fp = fsockopen("www.example.com", 80, $errno, $errstr, 30);
if (!$fp) {
    echo "$errstr ($errno)<br />\n";
} else {
    $out = "GET / HTTP/1.1\r\n";
    $out .= "Host: www.example.com\r\n";
    $out .= "Connection: Close\r\n\r\n";
    fwrite($fp, $out);
    while (!feof($fp)) {
        echo fgets($fp, 128);
    }
    fclose($fp);
}
?>

Básicamente he seguido este ejemplo que hace lo siguiente:

  1. Abre un socket, que es una conexión a un dominio.
  2. Prepara una salida $out para luego escribirla en ese socket. La salida se escribe siguiendo lo especificado en el protocolo HTTP, versión 1.1. Observe que después del Conection: Close debe llevar dos saltos de línea.
  3. Escribe en el socket con fwrite, es decir, envía la petición.
  4. El socket sigue abierto a la espera de que el servidor escriba su respuesta y nosotros podamos leearla en el bucle con fgets.
  5. Finalmente al llegar al final del fichero (del socket), salimos del bucle y cerramos la conexión.

Por motivos de seguridad no expongo este ejemplo en línea. Además su propósito es conocer un poco más este tema complejo de las redes y protocolos relacionados con Internet, pero es preferible ejecutarlo en local. Yo tengo instalado Apache+PHP bajo Windows en el dominio local para realizar pruebas, y es ahí donde creo que deben ejecutarse este tipo de aplicaciones "para experimentar". El código se presenta en el último apartado, donde puede copiarlo y usarlo en su localhost.

A continuación puede ver la parte superior de una captura de pantalla tal como se muestra esta aplicación en mi localhost, haciendo una petición a este sitio www.wextensible.com:

telnet php

He dispuesto dos desplegables para seleccionar el protocolo y el método de petición, aunque por ahora sólo contienen HTTP/1.1 y GET respectivamente. Luego podemos incluir el puerto (80 para HTTP) y el tiempo de espera, 30 segundos por defecto. El siguiente cuadro es para incluir la ruta, que debe ser relativa respecto al dominio. Por último el cuadro de cabeceras se usa para enviar, por ejemplo, las cookies.

Al pulsar el botón de enviar, el script recoge los campos del formulario y prepara una salida para escribir en el socket. Esta salida la presentamos en pantalla separando los saltos de línea con el caracter ⇒ para mayor comodidad. No debemos preocuparnos por los saltos de línea al final de las cabeceras introducidas, pues el script se encarga de eliminarlas y agregar los saltos que necesita. Por último se presenta la respuesta del servidor.

Lo que se recibe desde el servidor es el puro texto plano con las cabeceras de la página y el HTML. Cómo todas mis páginas están codificadas en UTF-8, hago una conversión de la respuesta del servidor con htmlentities($http_respuesta, ENT_QUOTES, "UTF-8") para poder ver los caracteres no ASCII. También convierto los mensajes de error con utf8_encode($errstr) para poder verlos adecuadamente en la página.

Código del Telnet con PHP

<?php
/* Ejemplo de un script PHP para hacer un TELNET
 * Andrés de la Paz © 2010
 * http://www.wextensible.com
 */

//Vemos si tiene magic_quotes activado el servidor
$magicq = (get_magic_quotes_gpc() == 1);

//Iniciamos las variables que contendrán los valores 
//enviados por el usuario
$un_dominio = "";
$un_protocolo = "HTTP/1.1";
$un_metodo = "GET";
$un_puerto = "80";
$un_timeout = "30";
$una_ruta = "/";
$una_cabecera = "";

//Saltos de línea para separar partes del envío según protocolo
define("SALTO", "\r\n");

//Iniciamos recogiendo los campos enviados por el usuario
foreach($_POST as $campo=>$valor){
    switch ($campo) {
        case "dominio":
            //La función htmlspecialchars() evita que
            //el usuario envíe caracteres no deseados
            $un_dominio = htmlspecialchars($valor, ENT_QUOTES);
            if ($magicq) $un_dominio = stripslashes($un_dominio);   
            //Quita todo espacio y saltos antes y después
            $un_dominio = trim($un_dominio);                    
            break;
        case "protocolo":
            $un_protocolo = htmlspecialchars($valor, ENT_QUOTES);
            if ($magicq) $un_protocolo = stripslashes($un_protocolo);
            break;
        case "metodo":
            $un_metodo = htmlspecialchars($valor, ENT_QUOTES);
            if ($magicq) $un_metodo = stripslashes($un_metodo);
            break;
        case "puerto":
            $un_puerto = htmlspecialchars($valor, ENT_QUOTES);
            if ($magicq) $un_puerto = stripslashes($un_puerto);
            break;
        case "timeout":
            $un_timeout = htmlspecialchars($valor, ENT_QUOTES);
            if ($magicq) $un_timeout = stripslashes($un_timeout);
            break;                                                  
        case "ruta":
            $una_ruta = htmlspecialchars($valor, ENT_QUOTES);
            if ($magicq) $una_ruta = stripslashes($una_ruta);
            //Quita todo espacio y saltos antes y después
            $una_ruta = trim($una_ruta);
            break;  
        case "cabecera":
            $una_cabecera = htmlspecialchars($valor, ENT_QUOTES);
            if ($magicq) $una_cabecera = stripslashes($una_cabecera);
            //Quita todo espacio y saltos antes y después
            $una_cabecera = trim($una_cabecera);
            break;                    
    }
} 
//Ahora componemos la página de salida
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
<head>
    <title>Telnet con PHP</title>
    <meta http-equiv="X-UA-Compatible" content="IE=8" />    
    <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
    ... 
</head>
<body>
    ...
    <h3>HACER UN TELNET CON PHP</h3>
    <form action="telnet.php" method="post">
        <p>El script eliminará todos los espacios y saltos antes y después
        del dominio, ruta y cabecera, agregándole a continuación los
        saltos de línea pertinentes. La ruta es relativa al dominio y no se
        incluirá método ni protocolo, que se agregará en el script según los
        seleccionados en los desplegables.
        No es necesario incluir <code>Connection: Close</code> al final de la 
        cabecera, pues será agregado en el script. 
        </p>    
        <label>Dominio:<br /><input type="text" name="dominio" class="codigo"
        value="<?php echo $un_dominio; ?>" size="100" /></label><br />
        <label>Protocolo:
        <select name="protocolo" class="codigo">
            <option value="HTTP/1.1" 
            <?php 
            if ($un_protocolo == "HTTP/1.1") echo " selected=\"selected\"";
            ?>
            >HTTP/1.1</option>
        </select>
        </label>
        <label> Método:
        <select name="metodo" class="codigo">
            <option value="GET" 
            <?php 
            if ($un_metodo == "GET") echo " selected=\"selected\"";
            ?>
            >GET</option>
        </select>
        </label>
        <label>Puerto:  <input type="text" name="puerto" class="codigo"
        value="<?php echo $un_puerto; ?>" size="5" /></label>
        <label>Timeout: <input type="text" name="timeout" class="codigo"
        value="<?php echo $un_timeout; ?>" size="3" /></label>                
        <br/>        
        <label>Ruta (sólo la ruta relativa, sin dominio, método ni protocolo):
        <br /><textarea name="ruta" rows="5" cols="100"  class="codigo"
        ><?php echo $una_ruta; ?></textarea></label><br />
        <label>Cabeceras:<br /><textarea name="cabecera" 
        rows="5" cols="100"  class="codigo"
        ><?php echo $una_cabecera; ?></textarea></label><br />
        <input type="submit" value="enviar" />
        <input type="reset" value="borrar" /><br />

    </form>
    <?php
        /* No se controla si el dominio existe. Puede hacerse con
         * checkdnsrr() pero sólo funciona con PHP 5.3.0 y mi versión
         * local es 5.2.13, por lo que no la incluyo.
         */
        if (($un_dominio != "")&&($una_ruta != "")){
            $http_respuesta = "";
            //Abre un socket para una conexión con un dominio
            $mi_telnet = @fsockopen($un_dominio, $un_puerto, $errno, $errstr, $un_timeout);
            if (!$mi_telnet){
                echo "<p style='color: red'>Error: "
                .utf8_encode($errno)." <big>La conexión no ha podido 
                realizarse:</big>".
                "<br />".utf8_encode($errstr)."</p>";
            } else {
                //Preparamos una cadena de petición HTTP
                $escribe = $un_metodo." ".$una_ruta." ".$un_protocolo.SALTO.
                "Host: ".$un_dominio.SALTO;
                //Si hay cabecera la agregamos
                if ($una_cabecera != "") $escribe .= $una_cabecera.SALTO;
                $escribe .= "Connection: Close".SALTO.SALTO;
                //Preparamos una cadena para presentar la petición recibida en 
                //el servidor, resaltando los saltos de línea
                $escribe_rn = str_replace(SALTO, "⇒".SALTO, $escribe);
                echo "<h3>Cadena de petición recibida en el servidor</h3>".
                "<p>En esta presentación de la cabecera recibida se agrega la flecha
                &rArr; para indicar donde hay un salto de línea. Al final tiene
                que haber 2 saltos de línea.</p>".
                "<pre>$escribe_rn</pre>";
                //Escribimos la petición en el socket                
                fwrite($mi_telnet, $escribe);
                //Leemos líneas del socket
                while (!feof($mi_telnet)){
                    $http_respuesta .= fgets($mi_telnet, 128);
                }
            }
            //Cerramos el socket
            @fclose($mi_telnet);
            //Limpiamos caracteres reservados
            $cadena_html = htmlentities($http_respuesta, ENT_QUOTES, "UTF-8");
            //Ponemos el contenido HTML del documento   
            echo "<h3>Respuesta desde el servidor</h3>".
            "<pre>".$cadena_html."</pre>";
        }
    ?>
</body>
</html>