Simulando peticiones Web con User-Agent personalizado usando PHP

Camilo Herrera
winkhostingla
Published in
10 min readJun 4, 2024

--

Photo by Philipp Katzenberger on Unsplash

Bienvenidos de nuevo a esta nueva entrega de contenido interesante para todo público! (Para todo público con anteojos y alergias)

Hoy vamos a crear una herramienta para simular peticiones web usando un User-Agent personalizado. Nos servirá para los siguientes casos (los que consideré al crearla):

  • Verificar el comportamiento de un sitio web al recibir una petición de cierto tipo de dispositivos, confirmar si realiza redirecciones, y el contenido HTML presentado.
  • Si tienes un WAF configurado en un sitio web, puedes confirmar si hay reglas de bloqueo o de restricción a cierto tipo de cadenas de User-Agent, esto puede ser de utilidad para mitigar algunos tipos de ataques o scan de vulnerabilidades conocidas.
  • También si aplicaste reglas en un archivo .htaccess asociados a la cadena de User-Agent en un sitio web, puedes probarlas usando esta herramienta.

Bueno ahora vamos a lo importante, nuestra solución. Si vienes leyendo mis publicaciones, notarás que sigo generalmente el mismo patrón y estructura para este tipo de pequeñas herramientas porque soy muy ecológico y me gusta reusar, reducir y reciclar código ;).

Ya con eso fuera del camino, vamos a proponer la estructura y requisitos de nuestra solución:

Requisitos

Como siempre vamos a usar un entorno de desarrollo con las siguientes herramientas:

  • Apache 2.4.x Nuestro viejo y conocido servidor web
  • PHP 8.3 (Puede funcionar sin problemas con PHP 8.2 y 8.1 solo es necesario hacer pruebas, yo lo hice con 8.3 y 8.2)
  • Extensión cURL activa y configurada con soporte para conexiones SSL (Esto es muy importante para sitios https, de lo contrario tendrás errores)
  • Bulma CSS, esta es la librería de GUI que utilizo normalmente, puedes visitarlos en https://bulma.io
  • Tu editor de código favorito (VS Code en mi caso)
  • Highlight.js, encontré esta librería al intentar aplicar resaltado de sintaxis al código HTML devuelto en la petición que realizaremos, bastante práctica, la usaremos para darle un toque de elegancia a la información mostrada, puedes visitarlos en https://highlightjs.org/
  • Paciencia, dedicación, ganas de aprender y 50 flexiones de pecho diarias.

Ya cubiertos los requisitos vamos a ver como será la estructura de nuestros archivos a usar:

Estructura de archivos

Los archivos que usaremos serán los siguientes:

  • Headersim.php: Archivo que contiene la clase encargada de la lógica y funciones para hacer las consultas con un User-Agent personalizado y procesar el contenido recibido desde un sitio web.
  • index.html: Nuestra interfaz gráfica será mostrada en este archivo, también será el encargado de enviar la petición al backend con el nombre del host a consultar y el texto del User-Agent a usar.
  • requestmananer.php: Un viejo conocido de otras publicaciones, este archivo recibirá la petición realizada desde index.html, instanciará la clase Headersim y retornará el resultado en formato JSON
  • testbench.php: Este pequeño amigo será usado para hacer pruebas sin interfaz web, no es necesario, pero te puede servir en otras soluciones o para depurar rutinas sin un navegador.

Crea un directorio que sea visible a través de tu servidor web y en él crea los archivos, en mi caso llamé el directorio “headersim” y su contenido quedará así:

/headersim/
│ Headersim.php
│ index.html
│ requestmanager.php
│ testbench.php

Ahora vamos a ir, uno a uno, recorriendo nuestros archivos para entender su contenido y utilidad.

Headersim.php

Nuestra alma, el motor de nuestra solución, el cantante principal, nuestro MVP… bueno esa es la idea, es el encargado de realizar las operaciones más importantes.

Esta clase cuenta con los siguientes atributos:

  • $result: es un arreglo encargado de almacenar los resultados de la consulta realizada, en este caso contendrá los elementos “headers”, “body”, “siteTitle”, “host”, “userAgent” y “httpCode”. Creo que los nombres son bastante claros, es decir que, después de realizar una petición, $result almacenará, las cabeceras, cuerpo, título, host, User-Agent y código HTTP devueltos por el sitio consultado.
  • $headers: Es una cadena de texto que contendrá, como su nombre lo indica…, las cabeceras retornadas por el sitio consultado y tienen (generalmente) la siguiente estructura:
HTTP/1.1 200 OK
Date: Wed, 29 May 2024 16:21:30 GMT
Server: Apache
Vary: Accept-Encoding
Content-Length: 3483
Content-Type: text/html; charset=UTF-8
  • $body: Esta cadena de texto contendrá la respuesta en HTML del sitio web, esto ayudará a determinar si al cambiar el User-Agent el HTML retornado cambia, por ejemplo.
  • $httpCode: Esta cadena de texto contendrá el código HTTP devuelto, normalmente si todo sale bien será un 200 (OK) pero si ocurre algún error, se verá reflejado en el cambio en este código.
  • $siteTitle: Contendrá el título del sitio (si lo tiene) extraído del $body y será la cadena entre los tags “<title></title>
  • $host: Guarda el host/IP al que se realiza la petición, viene del formulario en index.html
  • $userAgent: Guarda el User-Agent a usar en la petición y también viene del formulario en index.html

Ahora los métodos dentro de la clase:

  • __construct(): El constructor de nuestra clase, aquí definimos los valores por defecto de nuestros atributos.
  • sendRequest(): Este método es el encargado de ejecutar la consulta a un sitio web, recibe dos parámetros $host y $userAgent. El $host es la dirección o IP del sitio a consultar y $userAgent es la cadena de User-Agent a usar.
  • getHeaders(): Esta función recibe los mismos parámetros que sendRequest() pero realiza la petición específica de los headers del sitio web consultado.
  • getBody(): También recibe los mismos parámetros que sendRequest() pero retorna el cuerpo de la respuesta, es decir el HTML devuelto por el sitio al ser consultado.
  • extractSiteTitle(): Esta función extrae el título del sitio web del HTML devuelto por getBody(), lo extrae usando una expresión regular para capturar el texto entre los tags “<title></title>”.

Y es todo, ahora veamos como queda el archivo implementado en PHP

<?php

/**
* Clase Headersim
*
* Esta clase es responsable de enviar solicitudes HTTP a un host dado,
* recuperar encabezados y contenido del cuerpo, y extraer el título del sitio.
*/
class Headersim
{
/**
* @var array $result Array para almacenar los resultados de la solicitud HTTP.
*/
private array $result;

/**
* @var string $headers Cadena para almacenar los encabezados de la respuesta HTTP.
*/
private string $headers;

/**
* @var string $body Cadena para almacenar el cuerpo de la respuesta HTTP.
*/
private string $body;

/**
* @var string $httpCode Cadena para almacenar el código de respuesta HTTP.
*/
private string $httpCode;

/**
* @var string $siteTitle Cadena para almacenar el título del sitio extraído.
*/
private string $siteTitle;

/**
* @var string $host Cadena para almacenar el host de la solicitud HTTP.
*/
private string $host;

/**
* @var string $userAgent Cadena para almacenar el User-Agent de la solicitud HTTP.
*/
private string $userAgent;

/**
* Constructor de Headersim.
* Inicializa las propiedades de la clase.
*/
public function __construct()
{
$this->result = array();
$this->headers = "";
$this->body = "";
$this->httpCode = "";
$this->siteTitle = "";
$this->host = "";
$this->userAgent = "";
}

/**
* Envía una solicitud HTTP al host especificado con el User-Agent dado.
*
* @param string $host El host al que enviar la solicitud.
* @param string $userAgent La cadena User-Agent a usar para la solicitud.
* @return array El resultado de la solicitud HTTP, incluyendo encabezados, cuerpo, título del sitio, host, User-Agent y código HTTP.
*/
public function sendRequest(string $host, string $userAgent): array
{
$this->host = $host;
$this->userAgent = $userAgent;

$this->result["headers"] = $this->getHeaders($host, $userAgent);
$this->result["body"] = $this->getBody($host, $userAgent);
$this->result["siteTitle"] = $this->extractSiteTitle($this->body);
$this->result["host"] = $this->host;
$this->result["userAgent"] = $this->userAgent;
$this->result["httpCode"] = $this->httpCode;

return $this->result;
}

/**
* Recupera los encabezados de la respuesta HTTP del host especificado.
*
* @param string $host El host al que enviar la solicitud.
* @param string $userAgent La cadena User-Agent a usar para la solicitud.
* @return string Los encabezados de la respuesta HTTP.
*/
public function getHeaders(string $host, string $userAgent): string
{
$curl = curl_init();

curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 60);
curl_setopt($curl, CURLOPT_HEADER, true);
curl_setopt($curl, CURLOPT_NOBODY, true);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "GET");
curl_setopt($curl, CURLOPT_USERAGENT, $userAgent);
curl_setopt($curl, CURLOPT_URL, $host);

$this->headers = curl_exec($curl);
$this->httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);

return $this->headers;
}

/**
* Recupera el cuerpo de la respuesta HTTP del host especificado.
*
* @param string $host El host al que enviar la solicitud.
* @param string $userAgent La cadena User-Agent a usar para la solicitud.
* @return string El cuerpo de la respuesta HTTP.
*/
public function getBody(string $host, string $userAgent): string
{
$curl = curl_init();

curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 60);
curl_setopt($curl, CURLOPT_HEADER, 0);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "GET");
curl_setopt($curl, CURLOPT_USERAGENT, $userAgent);
curl_setopt($curl, CURLOPT_URL, $host);

$this->body = curl_exec($curl);
$this->httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);

return $this->body;
}

/**
* Extrae el título del sitio del cuerpo de la respuesta HTTP.
*
* @param string $body El cuerpo de la respuesta HTTP.
* @return string El título del sitio extraído.
*/
private function extractSiteTitle(string $body): string
{
$this->siteTitle = "NOT FOUND";

$arrMatches = array();
preg_match("/\<title\>.*\<\/title\>/", $body, $arrMatches, PREG_OFFSET_CAPTURE);

if (!empty($arrMatches)) {
if (isset($arrMatches[0])) {
if (isset($arrMatches[0][0])) {
$this->siteTitle = strip_tags($arrMatches[0][0]);
}
}
}

return $this->siteTitle;
}
}

index.html

Este archivo contiene nuestra interfaz de usuario, en resumen captura los parámetros de la consulta en dos campos de tipo texto, “host” y “userAgent”, este último es de tipo textarea. También le permite al usuario dar clic sobre el botón “Send Request” y este se encarga de enviar la información al archivo requestmanager.php a través de una petición POST.

El resultado de la consulta es retornado al archivo y mostrado en los controles dentro del código HTML, la interfaz lucirá así:

Ahora veamos el contenido del archivo. En él también notarás que incluimos la librería highlight.js para resaltar la sintaxis del código HTML retornado en la respuesta y nuestro querido Bulma CSS.

<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>User-Agent Simulator</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>


<script type="module">
window.addEventListener('load', (event) => {

document.querySelector(".sendRequest").addEventListener('click', (event) => {

event.currentTarget.classList.add('is-loading');
event.currentTarget.disabled = true;

document.querySelector(".result").parentElement.classList.add("is-hidden");
document.querySelector(".error").parentElement.classList.add("is-hidden");

const payload = JSON.stringify({
"host": document.querySelector(".host").value,
"userAgent": document.querySelector(".userAgent").value
});

fetch('requestmanager.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: payload,
})
.then(response => response.json())
.then(data => {

document.querySelector(".result").parentElement.classList.remove("is-hidden");

console.log(data)
console.log(data.headers)
console.log(data.body)

document.querySelector(".headers").innerText = data.headers;
document.querySelector(".siteTitle").innerText = data.siteTitle;
document.querySelector(".resHost").innerText = data.host;
document.querySelector(".resUserAgent").innerText = data.userAgent;
document.querySelector(".httpCode").innerText = data.httpCode;
document.querySelector(".body").textContent = data.body;
document.querySelector(".body").removeAttribute("data-highlighted");

hljs.highlightAll();

})
.catch((error) => {
document.querySelector(".error").parentElement.classList.remove("is-hidden");
document.querySelector(".error").innerText = error;
console.error('Error:', error);
}).finally(() => {
document.querySelector(".sendRequest").classList.remove('is-loading');
document.querySelector(".sendRequest").disabled = false;
});
});

});
</script>
</head>

<body>
<section class="section">
<div class="columns">
<div class="column">
<div class="field">
<label class="label">Host</label>
<div class="control">
<input class="input host" type="text" placeholder="Hostname/IP">
</div>
<p class="help">Type the hostname/IP</p>
</div>
<div class="field">
<label class="label">User-Agent</label>
<div class="control">
<textarea class="textarea userAgent" placeholder="User-Agent string"></textarea>
</div>
<p class="help">Type the User-Agent string that you want to use for the request and click the Send
Request button</p>
</div>
<div class="field">
<p class="control">
<button class="button is-black sendRequest">
Send Request
</button>
</p>
</div>
</div>
</div>
<div class="columns is-hidden">
<div class="column result">
<div class="columns">
<div class="column">

<article class="message">
<div class="message-header">
<p>HTTP Response Code</p>
</div>
<div class="message-body">
<div class="columns">
<div class="column httpCode">

</div>
</div>
</div>
</article>

<article class="message">
<div class="message-header">
<p>Host</p>
</div>
<div class="message-body">
<div class="columns">
<div class="column resHost">

</div>
</div>
</div>
</article>

<article class="message">
<div class="message-header">
<p>Site Title</p>
</div>
<div class="message-body">
<div class="columns">
<div class="column siteTitle">

</div>
</div>
</div>
</article>

<article class="message">
<div class="message-header">
<p>User-Agent</p>
</div>
<div class="message-body">
<div class="columns">
<div class="column resUserAgent">

</div>
</div>
</div>
</article>

<article class="message">
<div class="message-header">
<p>Headers</p>
</div>
<div class="message-body">
<div class="columns">
<div class="column headers">

</div>
</div>
</div>
</article>

<article class="message">
<div class="message-header">
<p>Body HTML</p>
</div>
<div class="message-body">
<div class="columns">
<div class="column">
<pre>
<code style="min-width: 100%; width: 0px; overflow: auto;" class="language-html body"></code>
</pre>
</div>
</div>
</div>
</article>

</div>
</div>

</div>
</div>
<div class="columns">
<div class="column is-hidden">
<div class="notification is-danger error has-text-centered">
</div>
</div>
</div>
</section>
</body>

</html>

Continuemos con el siguiente archivo, requestmanager.php.

requestmanager.php

Este archivo es el intermediario entre nuestra clase Headersim y las peticiones que provienen de la interfaz en index.html

Su funcionamiento es bastante simple, incluye el archivo Headersim.php decodifica los parámetros recibidos en la petición POST desde index.html, ejecuta la función sendRequest() del objeto y para finalizar captura la respuesta recibida y la retorna en formato JSON para ser mostrada en index.html

El contenido del archivo a continuación:

<?php

//Se incluye la definición de la clase a usar para nuestra consulta.
require("Headersim.php");

//Decodificamos los parámetros recibidos desde el archivo index.html y los almacenamos en el arreglo $paramsFetch
$paramsFetch = json_decode(
file_get_contents("php://input"),
true
);

//instanciamos nuestra clase
$headerSim = new Headersim();

//Enviamos el nombre del host y User-Agent a usar en la petición
$result = $headerSim->sendRequest($paramsFetch["host"], $paramsFetch["userAgent"]);

//A continuación retornamos la respuesta en formato JSON y finalizamos la ejecución.
$jsonResponse = json_encode($result);
echo $jsonResponse;
exit;

Y para finalizar hablemos del archivo testbench.php.

testbench.php

Este archivo es de utilidad para hacer pruebas de tu código antes de tener una interfaz web lista o si quieres ejecutar código en un proceso de depuración usando solo la línea de comandos en tu PC.

Su contenido es similar al mostrado en requestmanager.php pero no recibimos parámetros a través de POST dado que es usado desde línea de comandos, simplemente se escriben los valores literales para los parámetros en las funciones ejecutadas así:

<?php

//Se incluye la definición de la clase a usar para nuestra consulta.
require("Headersim.php");

//instanciamos nuestra clase
$headerSim = new Headersim();

//Ejecutamos la consulta
$result = $headerSim->sendRequest("https://winkhosting.co", "Googlebot/2.1 (+http://www.googlebot.com/bot.html)");

//Mostramos el contenido de la respuesta
print_r($result);

Ahora, si todo sale bien y copiaste el código correctamente, puedes hacer consultas simulando un User-Agent ingresando al URL donde se encuentran tus archivos, en mi caso http://localhost/headersim/index.html

Y es todo, no olvides que en winkhosting.co somos mucho más que Hosting!

P.D. No olvides las 50 flexiones de pecho!

--

--