<?php

class WebSocket {
	private $serv;
	private $timeout;
	private $client_descs = [];

	private $on_connect = null;
	private $on_disconnect = null;
	private $on_msg = null;
	private $on_timeout = null;

	public function __construct($host, $port, $timeout = 10000000) {
		$this->serv = new Server($host, $port);
		$this->timeout = $timeout;
	}

	public function start() {
		$this->serv->start($this, $this->timeout);
	}

	public function stop() {
		$this->serv->stop();
	}

	public function disconnect($id) {
		$this->serv->disconnect($id);
	}

	public function getClients() {
		$clients = array();

		foreach($this->client_descs as $id => $client)
			$clients[] = $id;

		return $clients;
	}

	public function on($action, Closure $func) {
		switch($action) {
			case 'connect':
				$this->on_connect = $func;
				break;
			case 'disconnect':
				$this->on_disconnect = $func;
				break;
			case 'msg':
				$this->on_msg = $func;
				break;
			case 'timeout':
				$this->on_timeout = $func;
				break;
		}
	}

	public function sendAll($msg) {
		foreach($this->client_descs as $client) {
			$client->send($msg);
		}
	}

	public function send($id, $msg) {
		$this->client_descs[$id]->send($msg);
	}

	public function sendHeartBeat() {
		foreach($this->client_descs as $client) {
			$client->heartbeat();
		}
	}

	/* Functions needed by Server */

	public function onConnect($id) {
		echo "WebSocket: Client connected.\n";
		$this->client_descs[$id] = new ClientDesc($this->serv, $id);
		$this->client_descs[$id]->connect();
		$callback = $this->on_connect;

		if($callback !== null)
			$callback($id);
	}

	public function onDisconnect($id) {
		echo "WebSocket: Client disconnected.\n";
		unset($this->client_descs[$id]);
		$callback = $this->on_disconnect;

		if($callback !== null)
			$callback($id);
	}

	public function onData($id, $data) {
		$msg_arr = $this->client_descs[$id]->onData($data);
		$callback = $this->on_msg;

		if($callback !== null)
			foreach($msg_arr as $msg)
				$callback($id, $msg);
	}

	public function onTimeout() {
		$this->sendHeartbeat();
		$callback = $this->on_timeout;

		if($callback !== null)
			$callback();
	}
}

class ClientDesc {
	private $id;
	private $serv;
	private $connected = false;
	private $handshake_done = false;
	private $reqBuf_remainlen = 0;
	private $reqBuf = null;
	private $payloadBuf = null;
	private $heartbeat_waiting = null;

	public function __construct($serv, $id) {
		$this->serv = $serv;
		$this->id = $id;
	}

	public function connect() {
		$this->connected = true;
	}

	public function disconnect() {
		if(!$this->connected)
			return;

		$req = $this->buildCloseReq('');
		$this->serv->send($this->id, $req);
		$this->serv->disconnect($this->id);
		$this->connected = false;
	}

	public function heartbeat() {
		if($this->heartbeat_waiting !== null) {
			echo "Heartbeat with no answer, diconnecting...\n";
			$this->disconnect();
			return;
		}

		$this->heartbeat_waiting = sha1('salt.f4cvgr41' . decbin(154 + rand()), false);
		$rq = $this->buildPingReq($this->heartbeat_waiting);
		$this->serv->send($this->id, $rq);
	}

	private function buildReq($type, $msg) {
		// We do not fragment.
		$req = $this->uchartobin($type & 0xF | 0x80);
		$len = strlen($msg);

		if($len <= 125) {
			$req .= $this->uchartobin($len & 0x7F);
		} else if($len <= 0xFFFF) {
			$req .= $this->uchartobin(126);
			$req .= $this->u16tobin($len & 0xFFFF);
		} else {
			$req .= $this->uchartobin(127);
			$req .= $this->u16tobin($len);
		}

		$req .= $msg;
		return $req;
	}

	private function buildContReq($msg) {
		return $this->buildReq(0x0, $msg);
	}

	private function buildTextReq($msg) {
		return $this->buildReq(0x1, $msg);
	}

	private function buildBinReq($msg) {
		return $this->buildReq(0x2, $msg);
	}

	private function buildCloseReq($msg) {
		return $this->buildReq(0x8, $msg);
	}

	private function buildPingReq($msg) {
		return $this->buildReq(0x9, $msg);
	}

	private function buildPongReq($msg) {
		return $this->buildReq(0xA, $msg);
	}

	public function send($msg) {
		$rq = $this->buildTextReq($msg);
		$this->serv->send($this->id, $rq);
	}

	public function onData($data) {
		$data_arr = array();

		if(!$this->handshake_done) {
			if(!$this->handshake($data))
				$this->serv->disconnect($this->id);
			
			$this->handshake_done = true;
			return $data_arr;
		}

		if($this->reqBuf !== NULL) {
			$this->reqBuf_remainlen -= strlen($data);

			if($this->reqBuf_remainlen > 0) {
				$this->reqBuf .= $data;
				return $data_arr;
			}

			$data = $this->reqBuf . $data;
			$this->reqBuf = NULL;
			$this->reqBuf_remainlen = 0;
		}

		while(true) {
			// Too short.
			if(strlen($data) < 2)
				return $data_arr;

			$idx = 0;
			$c = $this->bintouchar($data[$idx]);
			$opcode = $c & 0xF;
			$fin = $c >> 7;
			$idx++;

			$c = $this->bintouchar($data[$idx]);
			$payload_len = $c & 0x7F;
			$mask = ($c & 0x80) >> 7;
			$idx++;

			if(!$mask) {
				$this->disconnect();
				return $data_arr;
			}

			if($payload_len === 126) {
				$n = substr($data, $idx, 2);
				$idx += 2;
				$payload_len = $this->bintou16($n);
			} else if($payload_len === 127) {
				$n = substr($data, $idx, 8);
				$idx += 8;
				$payload_len = $this->bintou64($n);
			}

			$mask_key = null;

			if($mask) {
				$mask_key = str_split(substr($data, $idx, 4));
				$mask_key[0] = $this->bintouchar($mask_key[0]);
				$mask_key[1] = $this->bintouchar($mask_key[1]);
				$mask_key[2] = $this->bintouchar($mask_key[2]);
				$mask_key[3] = $this->bintouchar($mask_key[3]);
				$idx += 4;
			}

			$payload = substr($data, $idx, $payload_len);

			if(strlen($payload) < $payload_len) {
				$this->reqBuf = $data;
				$this->reqBuf_remainlen = $payload_len - strlen($payload);
				return $data_arr;
			}

			if($mask)
				for($i = 0 ; $i < $payload_len ; $i++)
					$payload[$i] = chr($this->bintouchar($payload[$i]) ^ $mask_key[$i % 4]);

			switch($opcode) {
				case 0: /* Continuation */
					$this->payloadBuf .= $payload;

					if($fin)
						$payload = $this->payloadBuf;
					else
						$payload = null;

					break;

				case 1: /* Text */
				case 2: /* Binary */
					$this->payloadBuf = $payload;

					if(!$fin)
						$payload = null;

					break;

				case 8: /* Close */
					// We must disconnect and return immediately.
					$this->disconnect();
					return $data_arr;

				case 9: /* Ping */
					$req = $this->buildPongReq($payload);
					$this->serv->send($this->id, $req);
					$payload = null;

					break;

				case 10: /* Pong */
					// If the returned hash matches, reset heartbeat, otherwise just ignore it.
					if($this->heartbeat_waiting === $payload) 
						$this->heartbeat_waiting = null;
					
					$payload = null;

					break;
			}

			if($payload) {
				$this->payloadBuf = null;
				$data_arr[] = $payload;
			}

			$data = substr($data, $idx + $payload_len);

			if(strlen($data) === 0)
				break;
		}

		return $data_arr;
	}

	private function handshake($header) {
		$header = explode("\r\n", $header);
		$req = explode(' ', $header[0]);

		if(count($req) !== 3 || $req[0] !== 'GET')
			return false;

		$http = explode('/', $req[2]);

		if($http[0] !== 'HTTP')
			return false;

		$httpver = explode('.', $http[1]);
		$httpver_major = intval($httpver[0]);
		$httpver_minor = intval($httpver[1]);

		if($httpver_major < 1 || ($httpver_major < 1 && $httpver_minor < 1))
			return false;

		$hdr_upgrade = null;
		$hdr_connection = null;
		$hdr_ws_key = null;
		$hdr_ws_version = null;

		foreach($header as $entry) {
			$entry = explode(':', $entry, 2);

			if(count($entry) != 2)
				continue;

			$entry[1] = trim($entry[1]);

			switch($entry[0]) {
				case 'Upgrade':
					$param = explode(', ', $entry[1]);

					if(in_array('websocket', $param, true))
						$hdr_upgrade = true;
					
					break;

				case 'Connection':
					$param = explode(', ', $entry[1]);

					if(in_array('Upgrade', $param, true))
						$hdr_connection = true;
					
					break;

				case 'Sec-WebSocket-Key':
					$hdr_ws_key = $entry[1];

					break;

				case 'Sec-WebSocket-Version':
					if(intval($entry[1]) === 13)
						$hdr_ws_version = true;
					break;
			}
		}

		if($hdr_upgrade === null
				|| $hdr_connection === null
				|| $hdr_ws_key === null
				|| $hdr_ws_version === null)
			return false;

		$ans =  "HTTP/1.1 101 Switching Protocols\r\n";
		$ans .= "Upgrade: websocket\r\n";
		$ans .= "Connection: Upgrade\r\n";
		$ans .= 'Sec-WebSocket-Accept: ' . base64_encode(sha1($hdr_ws_key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true)) . "\r\n";
		$ans .= "\r\n";

		$this->serv->send($this->id, $ans);
		return true;
	}


	private function bintouchar($str) {
		return unpack('Cb', $str)['b'];
	}

	private function bintou16($str) {
		return unpack('nw', $str)['w'];
	}

	private function bintou64($str) {
		$nums = unpack('Nhigh/Nlow', $str);

		return ($nums['high'] << 32) | $nums['low'];
	}

	private function uchartobin($c) {
		return pack('C', $c);
	}

	private function u16tobin($w) {
		return pack('n', $w);
	}

	private function u64tobin($q) {
		$low = $q & 0xFFFFFFFF;
		$high = $q >> 32;
		return pack('NN', $high, $low);
	}
}

Booti's booting...

Vous devez être inscrit pour répondre à ce sujet.