jsonstreamreader.php 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. <?php
  2. /**
  3. * StatusNet, the distributed open-source microblogging tool
  4. *
  5. * PHP version 5
  6. *
  7. * LICENCE: This program is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU Affero General Public License as published by
  9. * the Free Software Foundation, either version 3 of the License, or
  10. * (at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU Affero General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU Affero General Public License
  18. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. *
  20. * @category Plugin
  21. * @package StatusNet
  22. * @author Brion Vibber <brion@status.net>
  23. * @copyright 2010 StatusNet, Inc.
  24. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  25. * @link http://status.net/
  26. */
  27. class OAuthData
  28. {
  29. public $consumer_key, $consumer_secret, $token, $token_secret;
  30. }
  31. /**
  32. *
  33. */
  34. abstract class JsonStreamReader
  35. {
  36. const CRLF = "\r\n";
  37. public $id;
  38. protected $socket = null;
  39. protected $state = 'init'; // 'init', 'connecting', 'waiting', 'headers', 'active'
  40. public function __construct()
  41. {
  42. $this->id = get_class($this) . '.' . substr(md5(mt_rand()), 0, 8);
  43. }
  44. /**
  45. * Starts asynchronous connect operation...
  46. *
  47. * @fixme Can we do the open-socket fully async to? (need write select infrastructure)
  48. *
  49. * @param string $url
  50. */
  51. public function connect($url)
  52. {
  53. common_log(LOG_DEBUG, "$this->id opening connection to $url");
  54. $scheme = parse_url($url, PHP_URL_SCHEME);
  55. if ($scheme == 'http') {
  56. $rawScheme = 'tcp';
  57. } else if ($scheme == 'https') {
  58. $rawScheme = 'ssl';
  59. } else {
  60. // TRANS: Server exception thrown when an invalid URL scheme is detected.
  61. throw new ServerException(_m('Invalid URL scheme for HTTP stream reader.'));
  62. }
  63. $host = parse_url($url, PHP_URL_HOST);
  64. $port = parse_url($url, PHP_URL_PORT);
  65. if (!$port) {
  66. if ($scheme == 'https') {
  67. $port = 443;
  68. } else {
  69. $port = 80;
  70. }
  71. }
  72. $path = parse_url($url, PHP_URL_PATH);
  73. $query = parse_url($url, PHP_URL_QUERY);
  74. if ($query) {
  75. $path .= '?' . $query;
  76. }
  77. $errno = $errstr = null;
  78. $timeout = 5;
  79. //$flags = STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT;
  80. $flags = STREAM_CLIENT_CONNECT;
  81. // @fixme add SSL params
  82. $this->socket = stream_socket_client("$rawScheme://$host:$port", $errno, $errstr, $timeout, $flags);
  83. $this->send($this->httpOpen($host, $path));
  84. stream_set_blocking($this->socket, false);
  85. $this->state = 'waiting';
  86. }
  87. /**
  88. * Send some fun data off to the server.
  89. *
  90. * @param string $buffer
  91. */
  92. function send($buffer)
  93. {
  94. fwrite($this->socket, $buffer);
  95. }
  96. /**
  97. * Read next packet of data from the socket.
  98. *
  99. * @return string
  100. */
  101. function read()
  102. {
  103. $buffer = fread($this->socket, 65536);
  104. return $buffer;
  105. }
  106. /**
  107. * Build HTTP request headers.
  108. *
  109. * @param string $host
  110. * @param string $path
  111. * @return string
  112. */
  113. protected function httpOpen($host, $path)
  114. {
  115. $lines = array(
  116. "GET $path HTTP/1.1",
  117. "Host: $host",
  118. 'User-Agent: ' . HTTPClient::userAgent() . ' (TwitterBridgePlugin)',
  119. "Connection: close",
  120. "",
  121. ""
  122. );
  123. return implode(self::CRLF, $lines);
  124. }
  125. /**
  126. * Close the current connection, if open.
  127. */
  128. public function close()
  129. {
  130. if ($this->isConnected()) {
  131. common_log(LOG_DEBUG, "$this->id closing connection.");
  132. fclose($this->socket);
  133. $this->socket = null;
  134. }
  135. }
  136. /**
  137. * Are we currently connected?
  138. *
  139. * @return boolean
  140. */
  141. public function isConnected()
  142. {
  143. return $this->socket !== null;
  144. }
  145. /**
  146. * Send any sockets we're listening on to the IO manager
  147. * to wait for input.
  148. *
  149. * @return array of resources
  150. */
  151. public function getSockets()
  152. {
  153. if ($this->isConnected()) {
  154. return array($this->socket);
  155. }
  156. return array();
  157. }
  158. /**
  159. * Take a chunk of input over the horn and go go go! :D
  160. *
  161. * @param string $buffer
  162. */
  163. public function handleInput($socket)
  164. {
  165. if ($this->socket !== $socket) {
  166. // TRANS: Exception thrown when input from an inexpected socket is encountered.
  167. throw new Exception(_m('Got input from unexpected socket!'));
  168. }
  169. try {
  170. $buffer = $this->read();
  171. $lines = explode(self::CRLF, $buffer);
  172. foreach ($lines as $line) {
  173. $this->handleLine($line);
  174. }
  175. } catch (Exception $e) {
  176. common_log(LOG_ERR, "$this->id aborting connection due to error: " . $e->getMessage());
  177. fclose($this->socket);
  178. throw $e;
  179. }
  180. }
  181. protected function handleLine($line)
  182. {
  183. switch ($this->state)
  184. {
  185. case 'waiting':
  186. $this->handleLineWaiting($line);
  187. break;
  188. case 'headers':
  189. $this->handleLineHeaders($line);
  190. break;
  191. case 'active':
  192. $this->handleLineActive($line);
  193. break;
  194. default:
  195. // TRANS: Exception thrown when an invalid state is encountered in handleLine.
  196. // TRANS: %s is the invalid state.
  197. throw new Exception(sprintf(_m('Invalid state in handleLine: %s.'),$this->state));
  198. }
  199. }
  200. /**
  201. *
  202. * @param <type> $line
  203. */
  204. protected function handleLineWaiting($line)
  205. {
  206. $bits = explode(' ', $line, 3);
  207. if (count($bits) != 3) {
  208. // TRANS: Exception thrown when an invalid response line is encountered.
  209. // TRANS: %s is the invalid line.
  210. throw new Exception(sprintf(_m('Invalid HTTP response line: %s.'),$line));
  211. }
  212. list($http, $status, $text) = $bits;
  213. if (substr($http, 0, 5) != 'HTTP/') {
  214. // TRANS: Exception thrown when an invalid response line part is encountered.
  215. // TRANS: %1$s is the chunk, %2$s is the line.
  216. throw new Exception(sprintf(_m('Invalid HTTP response line chunk "%1$s": %2$s.'),$http, $line));
  217. }
  218. if ($status != '200') {
  219. // TRANS: Exception thrown when an invalid response code is encountered.
  220. // TRANS: %1$s is the response code, %2$s is the line.
  221. throw new Exception(sprintf(_m('Bad HTTP response code %1$s: %2$s.'),$status,$line));
  222. }
  223. common_log(LOG_DEBUG, "$this->id $line");
  224. $this->state = 'headers';
  225. }
  226. protected function handleLineHeaders($line)
  227. {
  228. if ($line == '') {
  229. $this->state = 'active';
  230. common_log(LOG_DEBUG, "$this->id connection is active!");
  231. } else {
  232. common_log(LOG_DEBUG, "$this->id read HTTP header: $line");
  233. $this->responseHeaders[] = $line;
  234. }
  235. }
  236. protected function handleLineActive($line)
  237. {
  238. if ($line == "") {
  239. // Server sends empty lines as keepalive.
  240. return;
  241. }
  242. $data = json_decode($line);
  243. if ($data) {
  244. $this->handleJson($data);
  245. } else {
  246. common_log(LOG_ERR, "$this->id received bogus JSON data: " . var_export($line, true));
  247. }
  248. }
  249. abstract protected function handleJson(stdClass $data);
  250. }