/Handshake.class.php
PHP | 283 lines | 200 code | 33 blank | 50 comment | 69 complexity | e0ab82685eeb63f3d0a7267287f36979 MD5 | raw file
- <?php
- // Representa uma operação de handshake
- // Um handshake é dito bem sucedido se a aplicação é não nula e o código de resposta é 101
- class Handshake {
- private $conexao = NULL; // Conexão pela qual o handshake está sendo feito
- private $aplicacao = NULL; // Aplicação escolhida para alocar a conexão (NULL para nenhuma)
- private $recurso = ''; // Armazena o recurso requisitado. Exemplo: '/chat'
- private $status = array(101, 'Switching Protocols'); // Armazena o código e descrição do status da resposta
-
- // Arrays associativas com os cabeçalhos da requisição e resposta, respectivamente
- // Todas as chaves e elementos são strings em minúscula
- private $requisicao = array();
- private $resposta = array();
-
- // Gera um handshake para a conexão
- // Retorna um LengthException caso ainda precise de dados
- // Se $conexao for NULL, cria um handshake vazio não associado a nenhuma conexão
- public function __construct() {}
-
- // Interpreta a parte do handshake enviado pelo cliente
- public function interpretarRequisicao(Conexao $conexao) {
- $this->conexao = $conexao;
- $str = $conexao->getBuffer();
-
- // Tamanho mínimo
- if (strlen($str) < 114)
- throw new LengthException('Fim inesperado');
-
- // Remove espaços adicionais
- $str = preg_replace('@(\r\n)?[ \t]+@', ' ', $str);
-
- // Primeira linha
- if (!preg_match('@^GET (.+) HTTP/(\d+)\.(\d+)\r\n@i', $str, $matches))
- $this->falhar(true);
- $M = (int)$matches[2];
- $m = (int)$matches[3];
- if ($M < 1 || ($M == 1 && $m < 1))
- $this->falhar(true);
- $this->recurso = $matches[1];
- $str = substr($str, strlen($matches[0]));
-
- // Cabeçalhos seguintes
- $padrao = '@^([!#-\'*+\-.0-9A-Z^_`a-z|~]+):(.*)\r\n@';
- $erro = false;
- while (substr($str, 0, 2) != "\r\n") {
- if (!preg_match($padrao, $str, $matches)) {
- // Formato incorreto. Depois será analisado o porquê
- $erro = true;
- break;
- }
- $chave = strtolower($matches[1]);
- $valor = trim($matches[2]);
- $str = substr($str, strlen($matches[0]));
- if (isset($this->requisicao[$chave]))
- $this->requisicao[$chave] .= ',' . $valor;
- else
- $this->requisicao[$chave] = $valor;
- }
-
- // Verifica se acabou bem ou não
- if ($erro)
- if (preg_match($padrao, $str . ":\r\n"))
- // Só tá faltando algumas partes, bora esperar
- throw new LengthException('Fim inesperado');
- else
- $this->falhar(true);
- $str = substr($str, 2);
-
- // Valida o handshake
- if (!isset($this->requisicao['host'])
- || !isset($this->requisicao['upgrade'])
- || !isset($this->requisicao['connection'])
- || !isset($this->requisicao['sec-websocket-key'])
- || !isset($this->requisicao['sec-websocket-version'])
- || strtolower($this->requisicao['upgrade']) != 'websocket'
- || stripos($this->requisicao['connection'], 'upgrade') === false
- || $this->requisicao['sec-websocket-version'] != '13'
- )
- $this->falhar(true);
-
- // Valida a chave
- $key = $this->requisicao['sec-websocket-key'];
- $decode = base64_decode($key, true);
- if ($decode === false || strlen($decode) != 16)
- $this->falhar(true);
-
- // Pega a aplicação
- $this->aplicacao = Servidor::getAplicacao($this->recurso);
- if (!$this->aplicacao)
- $this->falhar(true);
-
- // Monta a resposta
- $this->resposta = array(
- 'Upgrade' => 'websocket',
- 'Connection' => 'Upgrade',
- 'Sec-WebSocket-Accept' => base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true))
- );
-
- $conexao->setBuffer($str);
- }
-
- // Interpreta a parte do handshake enviado pelo servidor
- public function interpretarResposta(Conexao $conexao) {
- $this->conexao = $conexao;
- $str = $conexao->getBuffer();
-
- // Tamanho mínimo
- if (strlen($str) < 107)
- throw new LengthException('Fim inesperado');
-
- // Remove espaços adicionais
- $str = preg_replace('@(\r\n)?[ \t]+@', ' ', $str);
-
- // Primeira linha
- if (!preg_match('@^HTTP/(\d+)\.(\d+) (\d{3}) ([^\r\n]*)\r\n@i', $str, $matches))
- $this->falhar(false);
- $M = (int)$matches[1];
- $m = (int)$matches[2];
- if ($M < 1 || ($M == 1 && $m < 1))
- $this->falhar(false);
- $this->status = array($matches[3], $matches[4]);
- $str = substr($str, strlen($matches[0]));
-
- // Cabeçalhos seguintes
- $padrao = '@^([!#-\'*+\-.0-9A-Z^_`a-z|~]+):(.*)\r\n@';
- $erro = false;
- while (substr($str, 0, 2) != "\r\n") {
- if (!preg_match($padrao, $str, $matches)) {
- // Formato incorreto. Depois será analisado o porquê
- $erro = true;
- break;
- }
- $chave = strtolower($matches[1]);
- $valor = trim($matches[2]);
- $str = substr($str, strlen($matches[0]));
- if (isset($this->resposta[$chave]))
- $this->resposta[$chave] .= ',' . $valor;
- else
- $this->resposta[$chave] = $valor;
- }
-
- // Verifica se acabou bem ou não
- if ($erro)
- if (preg_match($padrao, $str . ":\r\n"))
- // Só tá faltando algumas partes, bora esperar
- throw new LengthException('Fim inesperado');
- else
- $this->falhar(false);
- $str = substr($str, 2);
-
- // Valida o handshake
- if ($this->status[0] == 101) {
- if (!isset($this->resposta['upgrade'])
- || !isset($this->resposta['connection'])
- || !isset($this->resposta['sec-websocket-accept'])
- || strtolower($this->resposta['upgrade']) != 'websocket'
- || stripos($this->resposta['connection'], 'upgrade') === false
- )
- $this->falhar(false);
-
- // Valida a chave
- $key = $this->requisicao['sec-websocket-key'];
- $accept = base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
- if ($this->resposta['sec-websocket-accept'] != $accept)
- $this->falhar(false);
-
- // Valida as extensões
- $in = $this->separarLista($this->requisicao, 'sec-websocket-extensions');
- $out = $this->separarLista($this->resposta, 'sec-websocket-extensions');
- foreach ($out as $cada)
- if (!in_array($cada, $in))
- $this->falhar(false);
-
- // Valida o protocolo
- if (isset($this->resposta['sec-websocket-protocol'])
- && !in_array($this->resposta['sec-websocket-protocol'], $this->separarLista($this->requisicao, 'sec-websocket-protocol')))
- $this->falhar(false);
- }
-
- $conexao->setBuffer($str);
- }
-
- // Retorna um cabeçalho da requisição (NULL para retornar uma array com todos)
- // Retorna NULL caso o cabeçalho não exista
- public function getRequisicao($header=NULL) {
- if ($header === NULL)
- return $this->requisicao;
- else if (isset($this->requisicao[$header]))
- return $this->requisicao[$header];
- else
- return NULL;
- }
-
- // Define o valor de um cabeçalho da requisição
- // Se o novo valor for NULL, o cabeçalho é deletado
- public function setRequisicao($header, $novo=NULL) {
- // Valida
- if (!preg_match('@^[!#-\'*+\-.0-9A-Z^_`a-z|~]+$@', $header))
- throw new Exception('Formato inválido');
- $header = strtolower($header);
- if ($novo === NULL)
- unset($this->requisicao[$header]);
- else if (preg_match('@^[^\r\n]*$@', $novo))
- $this->requisicao[$header] = (string)$novo;
- else
- throw new Exception('Valor inválido');
- }
-
- // Retorna um cabeçalho da resposta (NULL para retornar uma array com todos)
- // Retorna NULL caso o cabeçalho não exista
- public function getResposta($header=NULL) {
- if ($header === NULL)
- return $this->resposta;
- else if (isset($this->resposta[$header]))
- return $this->resposta[$header];
- else
- return NULL;
- }
-
- // Define o valor de um cabeçalho da resposta
- // Se o novo valor for NULL, o cabeçalho é deletado
- public function setResposta($header, $novo=NULL) {
- // Valida
- if (!preg_match('@^[!#-\'*+\-.0-9A-Z^_`a-z|~]+$@', $header))
- throw new Exception('Formato inválido');
- $header = strtolower($header);
- if ($novo === NULL)
- unset($this->resposta[$header]);
- else if (preg_match('@^[^\r\n]*$@', $novo))
- $this->resposta[$header] = (string)$novo;
- else
- throw new Exception('Valor inválido');
- }
-
- // Retorna a representação em string do handshake
- // Se $requisicao for true, retorna a requisição, caso contrário a resposta
- public function getString($requisicao) {
- if ($requisicao) {
- $recurso = $this->recurso;
- $str = "GET $recurso HTTP/1.1\r\n";
- } else {
- $status = $this->status[0] . ($this->status[1] ? ' ' . $this->status[1] : '');
- $str = "HTTP/1.1 $status \r\n";
- }
- foreach ($requisicao ? $this->requisicao : $this->resposta as $nome=>$valor)
- $str .= "$nome: $valor\r\n";
- return $str . "\r\n";
- }
-
- // Define os status e cabeçalhos de falha
- // Se $servidor for true, envia uma reposta de erro pela conexão
- private function falhar($servidor) {
- if ($servidor) {
- $this->status = array(400, 'Bad Request');
- $this->resposta = array('Upgrade' => 'websocket', 'Connection' => 'Upgrade');
- $this->conexao->enviarHandshake($this);
- }
- throw new Exception('Requsição inválida');
- }
-
- // Processa uma lista contida na $chave da $array
- // Retorna uma array de strings
- // Se a $chave não for encontrada, retorna uma array vazia
- private function separarLista($array, $chave) {
- $lista = array();
- if (isset($array[$chave]))
- foreach (explode(',', $array[$chave]) as $cada)
- $lista[] = strtolower(trim($cada));
- return $lista;
- }
-
- // Getters
- public function getConexao() {return $this->conexao;}
- public function getAplicacao() {return $this->aplicacao;}
- public function getRecurso() {return $this->recurso;}
- public function getStatusCode() {return $this->status[0];}
- public function getStatusReason() {return $this->status[1];}
-
- // Setters
- public function setAplicacao(Aplicacao $novo=NULL) {$this->aplicacao = $novo;}
- public function setRecurso($novo) {$this->recurso = (string)$novo;}
- public function setStatus($code, $reason='') {$this->status = array((int)$code, (string)$reason);}
- }