Created
June 18, 2012 19:20
-
-
Save nekudo/2950185 to your computer and use it in GitHub Desktop.
Monitoring PHP-CLI scripts with websockets
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
ini_set('display_errors', 1); | |
error_reporting(E_ALL); | |
/** | |
* Very basic websocket client. | |
* Supporting draft hybi-10. | |
* | |
* @author Simon Samtleben <[email protected]> | |
* @version 2011-10-18 | |
*/ | |
class WebsocketClient | |
{ | |
private $_host; | |
private $_port; | |
private $_path; | |
private $_origin; | |
private $_Socket = null; | |
private $_connected = false; | |
public function __construct() { } | |
public function __destruct() | |
{ | |
$this->disconnect(); | |
} | |
public function sendData($data, $type = 'text', $masked = true) | |
{ | |
if($this->_connected === false) | |
{ | |
return false; | |
} | |
if(empty($data)) | |
{ | |
return false; | |
} | |
$res = @fwrite($this->_Socket, $this->_hybi10Encode($data, $type, $masked)); | |
if($res === 0 || $res === false) | |
{ | |
return false; | |
} | |
$buffer = ' '; | |
while($buffer !== '') | |
{ | |
$buffer = fread($this->_Socket, 512); | |
} | |
return true; | |
} | |
public function connect($host, $port, $path, $origin = false) | |
{ | |
$this->_host = $host; | |
$this->_port = $port; | |
$this->_path = $path; | |
$this->_origin = $origin; | |
$key = base64_encode($this->_generateRandomString(16, false, true)); | |
$header = "GET " . $path . " HTTP/1.1\r\n"; | |
$header.= "Host: ".$host.":".$port."\r\n"; | |
$header.= "Upgrade: websocket\r\n"; | |
$header.= "Connection: Upgrade\r\n"; | |
$header.= "Sec-WebSocket-Key: " . $key . "\r\n"; | |
if($origin !== false) | |
{ | |
$header.= "Sec-WebSocket-Origin: " . $origin . "\r\n"; | |
} | |
$header.= "Sec-WebSocket-Version: 13\r\n"; | |
$this->_Socket = fsockopen($host, $port, $errno, $errstr, 2); | |
socket_set_timeout($this->_Socket, 0, 10000); | |
@fwrite($this->_Socket, $header); | |
$response = @fread($this->_Socket, 1500); | |
preg_match('#Sec-WebSocket-Accept:\s(.*)$#mU', $response, $matches); | |
$keyAccept = trim($matches[1]); | |
$expectedResonse = base64_encode(pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))); | |
$this->_connected = ($keyAccept === $expectedResonse) ? true : false; | |
return $this->_connected; | |
} | |
public function checkConnection() | |
{ | |
$this->_connected = false; | |
// send ping: | |
$data = 'ping?'; | |
@fwrite($this->_Socket, $this->_hybi10Encode($data, 'ping', true)); | |
$response = @fread($this->_Socket, 300); | |
if(empty($response)) | |
{ | |
return false; | |
} | |
$response = $this->_hybi10Decode($response); | |
if(!is_array($response)) | |
{ | |
return false; | |
} | |
if(!isset($response['type']) || $response['type'] !== 'pong') | |
{ | |
return false; | |
} | |
$this->_connected = true; | |
return true; | |
} | |
public function disconnect() | |
{ | |
$this->_connected = false; | |
fclose($this->_Socket); | |
} | |
public function reconnect() | |
{ | |
sleep(10); | |
$this->_connected = false; | |
fclose($this->_Socket); | |
$this->connect($this->_host, $this->_port, $this->_path, $this->_origin); | |
} | |
private function _generateRandomString($length = 10, $addSpaces = true, $addNumbers = true) | |
{ | |
$characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"§$%&/()=[]{}'; | |
$useChars = array(); | |
// select some random chars: | |
for($i = 0; $i < $length; $i++) | |
{ | |
$useChars[] = $characters[mt_rand(0, strlen($characters)-1)]; | |
} | |
// add spaces and numbers: | |
if($addSpaces === true) | |
{ | |
array_push($useChars, ' ', ' ', ' ', ' ', ' ', ' '); | |
} | |
if($addNumbers === true) | |
{ | |
array_push($useChars, rand(0,9), rand(0,9), rand(0,9)); | |
} | |
shuffle($useChars); | |
$randomString = trim(implode('', $useChars)); | |
$randomString = substr($randomString, 0, $length); | |
return $randomString; | |
} | |
private function _hybi10Encode($payload, $type = 'text', $masked = true) | |
{ | |
$frameHead = array(); | |
$frame = ''; | |
$payloadLength = strlen($payload); | |
switch($type) | |
{ | |
case 'text': | |
// first byte indicates FIN, Text-Frame (10000001): | |
$frameHead[0] = 129; | |
break; | |
case 'close': | |
// first byte indicates FIN, Close Frame(10001000): | |
$frameHead[0] = 136; | |
break; | |
case 'ping': | |
// first byte indicates FIN, Ping frame (10001001): | |
$frameHead[0] = 137; | |
break; | |
case 'pong': | |
// first byte indicates FIN, Pong frame (10001010): | |
$frameHead[0] = 138; | |
break; | |
} | |
// set mask and payload length (using 1, 3 or 9 bytes) | |
if($payloadLength > 65535) | |
{ | |
$payloadLengthBin = str_split(sprintf('%064b', $payloadLength), 8); | |
$frameHead[1] = ($masked === true) ? 255 : 127; | |
for($i = 0; $i < 8; $i++) | |
{ | |
$frameHead[$i+2] = bindec($payloadLengthBin[$i]); | |
} | |
// most significant bit MUST be 0 (close connection if frame too big) | |
if($frameHead[2] > 127) | |
{ | |
$this->close(1004); | |
return false; | |
} | |
} | |
elseif($payloadLength > 125) | |
{ | |
$payloadLengthBin = str_split(sprintf('%016b', $payloadLength), 8); | |
$frameHead[1] = ($masked === true) ? 254 : 126; | |
$frameHead[2] = bindec($payloadLengthBin[0]); | |
$frameHead[3] = bindec($payloadLengthBin[1]); | |
} | |
else | |
{ | |
$frameHead[1] = ($masked === true) ? $payloadLength + 128 : $payloadLength; | |
} | |
// convert frame-head to string: | |
foreach(array_keys($frameHead) as $i) | |
{ | |
$frameHead[$i] = chr($frameHead[$i]); | |
} | |
if($masked === true) | |
{ | |
// generate a random mask: | |
$mask = array(); | |
for($i = 0; $i < 4; $i++) | |
{ | |
$mask[$i] = chr(rand(0, 255)); | |
} | |
$frameHead = array_merge($frameHead, $mask); | |
} | |
$frame = implode('', $frameHead); | |
// append payload to frame: | |
$framePayload = array(); | |
for($i = 0; $i < $payloadLength; $i++) | |
{ | |
$frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i]; | |
} | |
return $frame; | |
} | |
private function _hybi10Decode($data) | |
{ | |
$payloadLength = ''; | |
$mask = ''; | |
$unmaskedPayload = ''; | |
$decodedData = array(); | |
// estimate frame type: | |
$firstByteBinary = sprintf('%08b', ord($data[0])); | |
$secondByteBinary = sprintf('%08b', ord($data[1])); | |
$opcode = bindec(substr($firstByteBinary, 4, 4)); | |
$isMasked = ($secondByteBinary[0] == '1') ? true : false; | |
$payloadLength = ord($data[1]) & 127; | |
switch($opcode) | |
{ | |
// text frame: | |
case 1: | |
$decodedData['type'] = 'text'; | |
break; | |
case 2: | |
$decodedData['type'] = 'binary'; | |
break; | |
// connection close frame: | |
case 8: | |
$decodedData['type'] = 'close'; | |
break; | |
// ping frame: | |
case 9: | |
$decodedData['type'] = 'ping'; | |
break; | |
// pong frame: | |
case 10: | |
$decodedData['type'] = 'pong'; | |
break; | |
default: | |
return false; | |
break; | |
} | |
if($payloadLength === 126) | |
{ | |
$mask = substr($data, 4, 4); | |
$payloadOffset = 8; | |
$dataLength = bindec(sprintf('%08b', ord($data[2])) . sprintf('%08b', ord($data[3]))) + $payloadOffset; | |
} | |
elseif($payloadLength === 127) | |
{ | |
$mask = substr($data, 10, 4); | |
$payloadOffset = 14; | |
$tmp = ''; | |
for($i = 0; $i < 8; $i++) | |
{ | |
$tmp .= sprintf('%08b', ord($data[$i+2])); | |
} | |
$dataLength = bindec($tmp) + $payloadOffset; | |
unset($tmp); | |
} | |
else | |
{ | |
$mask = substr($data, 2, 4); | |
$payloadOffset = 6; | |
$dataLength = $payloadLength + $payloadOffset; | |
} | |
if($isMasked === true) | |
{ | |
for($i = $payloadOffset; $i < $dataLength; $i++) | |
{ | |
$j = $i - $payloadOffset; | |
if(isset($data[$i])) | |
{ | |
$unmaskedPayload .= $data[$i] ^ $mask[$j % 4]; | |
} | |
} | |
$decodedData['payload'] = $unmaskedPayload; | |
} | |
else | |
{ | |
$payloadOffset = $payloadOffset - 4; | |
$decodedData['payload'] = substr($data, $payloadOffset); | |
} | |
return $decodedData; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
ini_set('display_errors', 1); | |
error_reporting(E_ALL); | |
require_once 'class.websocket_client.php'; | |
class WssLogger | |
{ | |
private $_WssClient = null; | |
public function __construct() | |
{ | |
$this->_WssClient = new WebsocketClient; | |
$this->_WssClient->connect('yourserver.com', 8000, '/logger', 'yoursource.com'); | |
} | |
public function log($msg, $source = 'unknown', $type = 'info') | |
{ | |
$logMsg = array( | |
'source' => $source, | |
'type' => $type, | |
'time' => date('H:i:s'), | |
'msg' => $msg, | |
); | |
$payload = json_encode(array( | |
'action' => 'log', | |
'data' => $logMsg, | |
)); | |
$sendResult = $this->_WssClient->sendData($payload); | |
if($sendResult === false) | |
{ | |
if($this->_WssClient->checkConnection() === false) | |
{ | |
$this->_WssClient->reconnect(); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<link rel="stylesheet" href="css/nanoscroller.css"> | |
<link rel="stylesheet" href="css/logger.css"> | |
<link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Inconsolata"> | |
<script src="js/jquery.min.js"></script> | |
<script src="js/json2.js"></script> | |
<script src="js/jquery.nanoscroller.min.js"></script> | |
<script src="js/logger.js"></script> | |
<meta charset=utf-8 /> | |
<title>Coplabs Logs</title> | |
</head> | |
<body> | |
<div id="container"> | |
<h1>Coplabs Logs</h1> | |
<span id="status" class="offline">disconnected</span> | |
<div id="main"> | |
<div id="console" class="nano"> | |
<div id="log" class="content"></div> | |
</div> | |
</div> | |
</div> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function() { | |
$(document).ready(function() { | |
var log, serverUrl, socket, writeLog; | |
log = function(msg) { | |
$('#log').prepend("<div class=\"logMsg\">" + msg + "</div>"); | |
while ($('.logMsg').length > 2000) { | |
$('#log div:last').remove(); | |
} | |
return $(".nano").nanoScroller(); | |
}; | |
serverUrl = 'ws://yourserver.com:8000/logger'; | |
if (window.MozWebSocket) { | |
socket = new MozWebSocket(serverUrl); | |
} else if (window.WebSocket) { | |
socket = new WebSocket(serverUrl); | |
} | |
socket.onopen = function(msg) { | |
return $('#status').removeClass().addClass('online').html('connected'); | |
}; | |
socket.onmessage = function(msg) { | |
var response; | |
response = JSON.parse(msg.data); | |
switch (response.action) { | |
case "writeLog": | |
return writeLog(response.data); | |
} | |
}; | |
socket.onclose = function(msg) { | |
return $('#status').removeClass().addClass('offline').html('disconnected'); | |
}; | |
$('#status').click(function() { | |
return socket.close(); | |
}); | |
writeLog = function(logData) { | |
var msgClass; | |
switch (logData.type) { | |
case "info": | |
msgClass = "appMsgInfo"; | |
break; | |
case "warn": | |
msgClass = "appMsgWarn"; | |
break; | |
case "notice1": | |
msgClass = "appMsgNoticeGreen"; | |
break; | |
case "notice2": | |
msgClass = "appMsgNoticeBlue"; | |
break; | |
case "notice3": | |
msgClass = "appMsgNoticePink"; | |
} | |
return log("<span class=\"time\">" + logData.time + "</span> <span class=\"appName\">" + logData.source + "</span> <span class=\"" + msgClass + "\">" + logData.msg + "</span>"); | |
}; | |
return $(".nano").nanoScroller(); | |
}); | |
}).call(this); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace WebSocket\Application; | |
/** | |
* Websocket-Server demo and test application. | |
* | |
* @author Simon Samtleben <[email protected]> | |
*/ | |
class LoggerApplication extends Application | |
{ | |
private $_clients = array(); | |
public function onConnect($client) | |
{ | |
$id = $client->getClientId(); | |
$this->_clients[$id] = $client; | |
} | |
public function onDisconnect($client) | |
{ | |
$id = $client->getClientId(); | |
unset($this->_clients[$id]); | |
} | |
public function onData($data, $client) | |
{ | |
$decodedData = $this->_decodeData($data); | |
if($decodedData === false) | |
{ | |
// @todo: invalid request trigger error... | |
} | |
$actionName = '_action' . ucfirst($decodedData['action']); | |
if(method_exists($this, $actionName)) | |
{ | |
call_user_func(array($this, $actionName), $decodedData['data']); | |
} | |
} | |
private function _actionLog($data) | |
{ | |
$encodedData = $this->_encodeData('writeLog', $data); | |
foreach($this->_clients as $sendto) | |
{ | |
$sendto->send($encodedData); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment