sept 28
Un Proxy HTTP multithreads en PHP !
Encore une classe éxotique qui sert surtout à montrer qu’on peut le faire, mais dont l’utilité n’est pas évidente
Enfin, moi j’en avais besoin dans le cadre d’un projet plus vaste…
Les seuls pseudo-proxies que j’avais trouvé en PHP étaient en général des usines à gaz qui scrappaient une page pour essayer ensuite de réécrire les liens, bref pas ce que j’appelle un proxy. Je me suis donc inspiré de ce qui existe en Python pour coder un proxy basé sur les Sockets. J’avais commencé par utiliser curl pour retranscrire les requêtes du client, mais il est apparu assez rapidement que c’était bien inutile : un code 100% socket étant plus fiable, plus léger et sans doute plus rapide (quoique la performance est loin d’être la marque de fabrique de ce script, PHP n’étant pas du tout adapté à ce genre d’usages).
Le script consiste en une classe de base, qui peut fonctionner telle quelle, il suffit de le lancer, idéalement en ligne de commande :
php proxy.php [host port]
Si le script est appelé sans paramètres, il se lance sur l’adresse 0.0.0.0, port 8000. Pratique pour le tester en local, il suffit alors de configurer le proxy dans votre navigateur (proxy HTTP / HTTPS, adresse 0.0.0.0, port 8000, et c’est tout !).
Ce proxy implémente la méthode CONNECT, c’est à dire que vous pouvez l’utiliser même sur des pages sécurisées (HTTPS). Il supporte à priori toutes les méthodes possibles : GET, POST, HEAD, PUT, DELETE. Tel quel, il agit comme un proxy 100% anonyme, c’est à dire que si vous éxécutez le script sur votre serveur et que vous l’utilisez comme proxy, vous pourrez surfer avec l’IP de votre serveur. Pour le vérifier vous pouvez consulter cette page : http://whatismyipaddress.com/staticpages/index.php/advanced-proxy-test qui vous indiquera que vous n’utilisez pas de proxy mais affichera bien l’IP de votre serveur et non la votre !
Comme indiqué plus haut, pour les perfs il faudra repasser
Pour améliorer ça, j’ai utilisé la fonction pcntl_fork() pour simuler le multithreading qui n’existe pas en PHP. Si les fonctions pcntl donc ne sont pas activées dans votre configuration PHP, le programme s’éxécute normalement et les requêtes se font donc une par une, à la chaine, ce qui donne une idée de ce à quoi doit ressembler le net en Afrique ou peut rappeler de vieux souvenirs à ceux qui sont nés avant l’invention de l’ADSL !
J’ai bien introduit une vague limitation du nombre de clients, mais il faudrait plutôt modifier ça pour limiter le nombre de processus fils engendrés simultanément si on veut éviter une catastrophe, ce qui n’est pas dur à faire, mais de toutes façon je vous rappelle pour ceux qui ne l’auraient pas compris qu’il ne faut pas utiliser ce proxy en PROD ou le laisser en accès libre sur votre serveur ! Le script est avant tout expérimental et il n’est pas sécure du tout en l’état. Si vous voulez utiliser votre serveur pour offrir des services de proxy au public, il existe de vraies solutions pour cela (SQUID par exemple). Si vous voulez juste l’utiliser pour vous même, je vous conseille au moins d’étendre la classe pour y ajouter un contrôle des clients (n’autoriser que certaines IP à utiliser le proxy, ou y ajouter un login/password).
Voilà le script, vous pouvez aussi le récupérer avec le show_source() de la dernière version ici : http://fbparis.com/proxy.php.
class BaseProxy {
protected $addr = NULL;
protected $port = NULL;
protected $debug = false;
protected $clients = array();
protected $buffer = array();
public $maxConnections = 50;
public $pid = -1;
function __construct($addr=‘0.0.0.0′,$port=‘8000′) {
$this->debug = defined(‘DEBUG’) ? DEBUG : false;
$this->log(‘__construct()’);
set_time_limit(0);
if (defined(‘THREADING’) && (THREADING == true)) {
if (!extension_loaded(‘pcntl’)) {
$prefix = (PHP_SHLIB_SUFFIX === ‘dll’) ? ‘php_’ : »;
if (!@dl($prefix . ‘pcntl.’ . PHP_SHLIB_SUFFIX)) $this->pid = -2;
}
}
else {
$this->pid = -2;
}
$this->addr = $addr;
$this->port = $port;
try {
$this->socket = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
if ($this->socket === false) $this->halt(‘Unable to create main socket’);
socket_set_option($this->socket,SOL_SOCKET,SO_REUSEADDR,1);
if (!socket_set_nonblock($this->socket)) $this->log($this->lastError());
if (!socket_bind($this->socket,$this->addr,$this->port)) $this->halt($this->lastError());
socket_getsockname($this->socket,$this->addr,$this->port);
$this->log(sprintf(‘Starting HTTP Proxy on %s port %s’,$this->addr,$this->port));
if (!socket_listen($this->socket)) $this->halt($this->lastError());
}
catch (Exception $e) {
$this->halt($e->getMessage());
}
$this->main();
}
function __destruct() {
$this->log(‘__destruct()’);
if ($this->pid !== 0) {
socket_close($this->socket);
foreach ($this->clients as $s) $this->destroy($s);
$this->clients = array();
$this->buffer = array();
}
posix_kill(getmypid(),9);
}
public function lastError($s=NULL) {
if ($s === NULL) $s =& $this->socket;
return @socket_strerror(socket_last_error($s));
}
public function main() {
$write = NULL;
while (true) {
if ((count($this->clients) <= $this->maxConnections) && ($s = @socket_accept($this->socket))) {
socket_getpeername($s,$raddr,$rport);
$this->log("Received Connection from $raddr:$rport");
$this->buffer[$s] = »;
if (!socket_set_block($s)) $this->log($this->lastError($s));
if (!socket_set_option($s,SOL_SOCKET,SO_RCVTIMEO,array(’sec’=>10,‘usec’=>0))) $this->log($this->lastError($s));
if (!socket_set_option($s,SOL_SOCKET,SO_SNDTIMEO,array(’sec’=>10,‘usec’=>0))) $this->log($this->lastError($s));
$this->clients[$s] = $s;
$this->log(sprintf(‘%s active connections’,count($this->clients)));
}
$read = $except = array_values($this->clients);
if (@socket_select($read,$write,$except,0)) {
foreach ($read as $s) {
$this->read($s);
}
foreach ($except as $s) {
$this->log($this->lastError($s));
$this->destroy($s);
}
}
usleep(100000);
}
}
protected function destroy($s) {
socket_close($s);
unset($this->clients[$s]);
unset($this->buffer[$s]);
}
protected function read($s) {
$this->buffer[$s] .= socket_read($s,8192,PHP_BINARY_READ);
if (preg_match(‘#\r\n\r\n$#s’,$this->buffer[$s])) {
if (trim($this->buffer[$s]) == ‘QUIT’) exit;
unset($this->clients[$s]);
if ($this->pid != -2) $this->pid = pcntl_fork();
if ($this->pid <= 0) $this->handle($s);
}
}
protected function handle($s) {
$req = preg_split(‘#\r\n#s’,$this->buffer[$s],-1);
unset($this->buffer[$s]);
list($method,$url,$protocol) = sscanf(array_shift($req),‘%s %s %s’);
$this->log("handle() :: $method $url $protocol");
switch ($method) {
case ‘CONNECT’:
$this->do_CONNECT($s,$url,$protocol,$req);
break;
case ‘GET’:
case ‘POST’:
case ‘DELETE’:
case ‘PUT’:
case ‘HEAD’:
$this->do_GET($s,$method,$url,$protocol,$req);
break;
default:
$this->do_ERROR($s,‘Not Implemented’,501,$protocol,$req);
break;
}
$this->log(‘handle() :: done’);
if ($this->pid === 0) {
$this->log(‘Killing child process’);
posix_kill(getmypid(),9);
exit;
}
}
protected function do_ERROR($s,$errmsg,$errcode,$protocol,$req) {
$this->log(sprintf(‘do_ERROR(%s,%s)’,$errmsg,$errcode));
$html = "<html><head><title>$errcode $errmsg</title></head><body><h1>$errmsg</h1></body></html>";
$length = strlen($html);
socket_write($s,"Status: $errcode $errmsg\r\nContent-Type: text/html\r\nContent-Length: $length\r\n\r\n$html");
$this->destroy($s);
}
protected function do_CONNECT($s,$url,$protocol,$req) {
$this->log(sprintf(‘do_CONNECT(%s)’,$url));
list($host,$port) = explode(‘:’,$url,2);
$d = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
if (($d === false) || !@socket_connect($d,$host,$port)) {
if ($d) $this->log($this->lastError($d));
@socket_close($d);
$this->do_ERROR($s,‘Not Found’,404,$protocol,$req);
}
else {
socket_write($s,"$protocol 200 Connection established\r\n\r\n");
$this->read_write($s,$d,30);
}
}
protected function do_GET($s,$method,$url,$protocol,$req) {
$this->log(sprintf(‘do_GET(%s,%s)’,$method,$url));
@extract(parse_url($url));
if ((strtolower($scheme) != ‘http’) || $fragment || !$host || !$path) {
$this->do_ERROR($s,‘Bad Request’,400,$protocol,$req);
}
else {
if (!$port) $port = 80;
$d = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
if (($d == false) || !@socket_connect($d,$host,$port)) {
if ($d) $this->log($this->lastError($d));
@socket_close($d);
$this->do_ERROR($s,‘Not Found’,404,$protocol,$req);
}
else {
// todo : auth requests
if ($query) $query = "?$query";
socket_write($d,sprintf("%s %s %s\r\n",$method,$path.$query,$protocol));
foreach ($req as $h) {
if (empty($h)) socket_write($d,"Connection: close\r\n\r\n");
elseif (!preg_match(‘#^(Proxy-Connection)|(Connection ?:)#si’,$h)) socket_write($d,"$h\r\n");
}
$this->read_write($s,$d);
}
}
}
protected function read_write($client,$server,$max_idling=20) {
$this->log(‘read_write()’);
if (!socket_set_block($server)) $this->log($this->lastError($server));
if (!socket_set_option($server,SOL_SOCKET,SO_RCVTIMEO,array(’sec’=>10,‘usec’=>0))) $this->log($this->lastError($server));
if (!socket_set_option($server,SOL_SOCKET,SO_SNDTIMEO,array(’sec’=>10,‘usec’=>0))) $this->log($this->lastError($server));
$n = 0;
$write = NULL;
while (true) {
$n++;
$read = $except = array($client,$server);
if (socket_select($read,$write,$except,3,0)) {
foreach ($read as $in) {
$data = @socket_read($in,8192,PHP_BINARY_READ);
if ($data === false) {
$this->log($this->lastError($in));
break;
}
elseif ($data) {
if (!@socket_write($in == $client ? $server : $client,$data)) {
$this->log($this->lastError($in == $client ? $server : $client));
break;
}
else $n = 0;
}
}
if (count($except)) {
foreach ($except as $e) $this->log($this->lastError($e));
break;
}
}
if ($n >= $max_idling) break;
}
$this->destroy($client);
$this->destroy($server);
}
protected function log($msg) {
if ($this->debug) printf("%s %s [PID=%s/%s]\n",date(‘Y-m-d H:i:s’),$msg,getmypid(),$this->pid);
}
protected function halt($errmsg) {
die("$errmsg\n");
}
}
define(‘DEBUG’,true);
define(‘THREADING’,true);
$proxy = $argc == 3 ? new BaseProxy($argv[1],$argv[2]) : new BaseProxy;
?>

9 novembre 2009 @ 13:47
Un complément pour gagner en performance
http://www.onlineaspect.com/2009/01/26/how-to-use-curl_multi-without-blocking/ ?
9 novembre 2009 @ 14:13
Curl n’est pas utilisé ici donc ce serait intéressant que tu nous expliques en quoi on gagner en performance