PageRenderTime 51ms CodeModel.GetById 27ms RepoModel.GetById 0ms app.codeStats 0ms

/Handshake.class.php

https://github.com/sitegui/websocket
PHP | 283 lines | 200 code | 33 blank | 50 comment | 69 complexity | e0ab82685eeb63f3d0a7267287f36979 MD5 | raw file
  1. <?php
  2. // Representa uma operação de handshake
  3. // Um handshake é dito bem sucedido se a aplicação é não nula e o código de resposta é 101
  4. class Handshake {
  5. private $conexao = NULL; // Conexão pela qual o handshake está sendo feito
  6. private $aplicacao = NULL; // Aplicação escolhida para alocar a conexão (NULL para nenhuma)
  7. private $recurso = ''; // Armazena o recurso requisitado. Exemplo: '/chat'
  8. private $status = array(101, 'Switching Protocols'); // Armazena o código e descrição do status da resposta
  9. // Arrays associativas com os cabeçalhos da requisição e resposta, respectivamente
  10. // Todas as chaves e elementos são strings em minúscula
  11. private $requisicao = array();
  12. private $resposta = array();
  13. // Gera um handshake para a conexão
  14. // Retorna um LengthException caso ainda precise de dados
  15. // Se $conexao for NULL, cria um handshake vazio não associado a nenhuma conexão
  16. public function __construct() {}
  17. // Interpreta a parte do handshake enviado pelo cliente
  18. public function interpretarRequisicao(Conexao $conexao) {
  19. $this->conexao = $conexao;
  20. $str = $conexao->getBuffer();
  21. // Tamanho mínimo
  22. if (strlen($str) < 114)
  23. throw new LengthException('Fim inesperado');
  24. // Remove espaços adicionais
  25. $str = preg_replace('@(\r\n)?[ \t]+@', ' ', $str);
  26. // Primeira linha
  27. if (!preg_match('@^GET (.+) HTTP/(\d+)\.(\d+)\r\n@i', $str, $matches))
  28. $this->falhar(true);
  29. $M = (int)$matches[2];
  30. $m = (int)$matches[3];
  31. if ($M < 1 || ($M == 1 && $m < 1))
  32. $this->falhar(true);
  33. $this->recurso = $matches[1];
  34. $str = substr($str, strlen($matches[0]));
  35. // Cabeçalhos seguintes
  36. $padrao = '@^([!#-\'*+\-.0-9A-Z^_`a-z|~]+):(.*)\r\n@';
  37. $erro = false;
  38. while (substr($str, 0, 2) != "\r\n") {
  39. if (!preg_match($padrao, $str, $matches)) {
  40. // Formato incorreto. Depois será analisado o porquê
  41. $erro = true;
  42. break;
  43. }
  44. $chave = strtolower($matches[1]);
  45. $valor = trim($matches[2]);
  46. $str = substr($str, strlen($matches[0]));
  47. if (isset($this->requisicao[$chave]))
  48. $this->requisicao[$chave] .= ',' . $valor;
  49. else
  50. $this->requisicao[$chave] = $valor;
  51. }
  52. // Verifica se acabou bem ou não
  53. if ($erro)
  54. if (preg_match($padrao, $str . ":\r\n"))
  55. // Só tá faltando algumas partes, bora esperar
  56. throw new LengthException('Fim inesperado');
  57. else
  58. $this->falhar(true);
  59. $str = substr($str, 2);
  60. // Valida o handshake
  61. if (!isset($this->requisicao['host'])
  62. || !isset($this->requisicao['upgrade'])
  63. || !isset($this->requisicao['connection'])
  64. || !isset($this->requisicao['sec-websocket-key'])
  65. || !isset($this->requisicao['sec-websocket-version'])
  66. || strtolower($this->requisicao['upgrade']) != 'websocket'
  67. || stripos($this->requisicao['connection'], 'upgrade') === false
  68. || $this->requisicao['sec-websocket-version'] != '13'
  69. )
  70. $this->falhar(true);
  71. // Valida a chave
  72. $key = $this->requisicao['sec-websocket-key'];
  73. $decode = base64_decode($key, true);
  74. if ($decode === false || strlen($decode) != 16)
  75. $this->falhar(true);
  76. // Pega a aplicação
  77. $this->aplicacao = Servidor::getAplicacao($this->recurso);
  78. if (!$this->aplicacao)
  79. $this->falhar(true);
  80. // Monta a resposta
  81. $this->resposta = array(
  82. 'Upgrade' => 'websocket',
  83. 'Connection' => 'Upgrade',
  84. 'Sec-WebSocket-Accept' => base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true))
  85. );
  86. $conexao->setBuffer($str);
  87. }
  88. // Interpreta a parte do handshake enviado pelo servidor
  89. public function interpretarResposta(Conexao $conexao) {
  90. $this->conexao = $conexao;
  91. $str = $conexao->getBuffer();
  92. // Tamanho mínimo
  93. if (strlen($str) < 107)
  94. throw new LengthException('Fim inesperado');
  95. // Remove espaços adicionais
  96. $str = preg_replace('@(\r\n)?[ \t]+@', ' ', $str);
  97. // Primeira linha
  98. if (!preg_match('@^HTTP/(\d+)\.(\d+) (\d{3}) ([^\r\n]*)\r\n@i', $str, $matches))
  99. $this->falhar(false);
  100. $M = (int)$matches[1];
  101. $m = (int)$matches[2];
  102. if ($M < 1 || ($M == 1 && $m < 1))
  103. $this->falhar(false);
  104. $this->status = array($matches[3], $matches[4]);
  105. $str = substr($str, strlen($matches[0]));
  106. // Cabeçalhos seguintes
  107. $padrao = '@^([!#-\'*+\-.0-9A-Z^_`a-z|~]+):(.*)\r\n@';
  108. $erro = false;
  109. while (substr($str, 0, 2) != "\r\n") {
  110. if (!preg_match($padrao, $str, $matches)) {
  111. // Formato incorreto. Depois será analisado o porquê
  112. $erro = true;
  113. break;
  114. }
  115. $chave = strtolower($matches[1]);
  116. $valor = trim($matches[2]);
  117. $str = substr($str, strlen($matches[0]));
  118. if (isset($this->resposta[$chave]))
  119. $this->resposta[$chave] .= ',' . $valor;
  120. else
  121. $this->resposta[$chave] = $valor;
  122. }
  123. // Verifica se acabou bem ou não
  124. if ($erro)
  125. if (preg_match($padrao, $str . ":\r\n"))
  126. // Só tá faltando algumas partes, bora esperar
  127. throw new LengthException('Fim inesperado');
  128. else
  129. $this->falhar(false);
  130. $str = substr($str, 2);
  131. // Valida o handshake
  132. if ($this->status[0] == 101) {
  133. if (!isset($this->resposta['upgrade'])
  134. || !isset($this->resposta['connection'])
  135. || !isset($this->resposta['sec-websocket-accept'])
  136. || strtolower($this->resposta['upgrade']) != 'websocket'
  137. || stripos($this->resposta['connection'], 'upgrade') === false
  138. )
  139. $this->falhar(false);
  140. // Valida a chave
  141. $key = $this->requisicao['sec-websocket-key'];
  142. $accept = base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
  143. if ($this->resposta['sec-websocket-accept'] != $accept)
  144. $this->falhar(false);
  145. // Valida as extensões
  146. $in = $this->separarLista($this->requisicao, 'sec-websocket-extensions');
  147. $out = $this->separarLista($this->resposta, 'sec-websocket-extensions');
  148. foreach ($out as $cada)
  149. if (!in_array($cada, $in))
  150. $this->falhar(false);
  151. // Valida o protocolo
  152. if (isset($this->resposta['sec-websocket-protocol'])
  153. && !in_array($this->resposta['sec-websocket-protocol'], $this->separarLista($this->requisicao, 'sec-websocket-protocol')))
  154. $this->falhar(false);
  155. }
  156. $conexao->setBuffer($str);
  157. }
  158. // Retorna um cabeçalho da requisição (NULL para retornar uma array com todos)
  159. // Retorna NULL caso o cabeçalho não exista
  160. public function getRequisicao($header=NULL) {
  161. if ($header === NULL)
  162. return $this->requisicao;
  163. else if (isset($this->requisicao[$header]))
  164. return $this->requisicao[$header];
  165. else
  166. return NULL;
  167. }
  168. // Define o valor de um cabeçalho da requisição
  169. // Se o novo valor for NULL, o cabeçalho é deletado
  170. public function setRequisicao($header, $novo=NULL) {
  171. // Valida
  172. if (!preg_match('@^[!#-\'*+\-.0-9A-Z^_`a-z|~]+$@', $header))
  173. throw new Exception('Formato inválido');
  174. $header = strtolower($header);
  175. if ($novo === NULL)
  176. unset($this->requisicao[$header]);
  177. else if (preg_match('@^[^\r\n]*$@', $novo))
  178. $this->requisicao[$header] = (string)$novo;
  179. else
  180. throw new Exception('Valor inválido');
  181. }
  182. // Retorna um cabeçalho da resposta (NULL para retornar uma array com todos)
  183. // Retorna NULL caso o cabeçalho não exista
  184. public function getResposta($header=NULL) {
  185. if ($header === NULL)
  186. return $this->resposta;
  187. else if (isset($this->resposta[$header]))
  188. return $this->resposta[$header];
  189. else
  190. return NULL;
  191. }
  192. // Define o valor de um cabeçalho da resposta
  193. // Se o novo valor for NULL, o cabeçalho é deletado
  194. public function setResposta($header, $novo=NULL) {
  195. // Valida
  196. if (!preg_match('@^[!#-\'*+\-.0-9A-Z^_`a-z|~]+$@', $header))
  197. throw new Exception('Formato inválido');
  198. $header = strtolower($header);
  199. if ($novo === NULL)
  200. unset($this->resposta[$header]);
  201. else if (preg_match('@^[^\r\n]*$@', $novo))
  202. $this->resposta[$header] = (string)$novo;
  203. else
  204. throw new Exception('Valor inválido');
  205. }
  206. // Retorna a representação em string do handshake
  207. // Se $requisicao for true, retorna a requisição, caso contrário a resposta
  208. public function getString($requisicao) {
  209. if ($requisicao) {
  210. $recurso = $this->recurso;
  211. $str = "GET $recurso HTTP/1.1\r\n";
  212. } else {
  213. $status = $this->status[0] . ($this->status[1] ? ' ' . $this->status[1] : '');
  214. $str = "HTTP/1.1 $status \r\n";
  215. }
  216. foreach ($requisicao ? $this->requisicao : $this->resposta as $nome=>$valor)
  217. $str .= "$nome: $valor\r\n";
  218. return $str . "\r\n";
  219. }
  220. // Define os status e cabeçalhos de falha
  221. // Se $servidor for true, envia uma reposta de erro pela conexão
  222. private function falhar($servidor) {
  223. if ($servidor) {
  224. $this->status = array(400, 'Bad Request');
  225. $this->resposta = array('Upgrade' => 'websocket', 'Connection' => 'Upgrade');
  226. $this->conexao->enviarHandshake($this);
  227. }
  228. throw new Exception('Requsição inválida');
  229. }
  230. // Processa uma lista contida na $chave da $array
  231. // Retorna uma array de strings
  232. // Se a $chave não for encontrada, retorna uma array vazia
  233. private function separarLista($array, $chave) {
  234. $lista = array();
  235. if (isset($array[$chave]))
  236. foreach (explode(',', $array[$chave]) as $cada)
  237. $lista[] = strtolower(trim($cada));
  238. return $lista;
  239. }
  240. // Getters
  241. public function getConexao() {return $this->conexao;}
  242. public function getAplicacao() {return $this->aplicacao;}
  243. public function getRecurso() {return $this->recurso;}
  244. public function getStatusCode() {return $this->status[0];}
  245. public function getStatusReason() {return $this->status[1];}
  246. // Setters
  247. public function setAplicacao(Aplicacao $novo=NULL) {$this->aplicacao = $novo;}
  248. public function setRecurso($novo) {$this->recurso = (string)$novo;}
  249. public function setStatus($code, $reason='') {$this->status = array((int)$code, (string)$reason);}
  250. }