575 changed files with 76309 additions and 0 deletions
@ -0,0 +1,179 @@ |
|||||||
|
<?php |
||||||
|
require_once 'config.php'; |
||||||
|
|
||||||
|
/** |
||||||
|
* @fileName comm |
||||||
|
* @author Amir <amirsanni@gmail.com> |
||||||
|
* @date 22-Dec-2016 |
||||||
|
*/ |
||||||
|
|
||||||
|
?> |
||||||
|
|
||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"> |
||||||
|
|
||||||
|
<title>Chat App</title> |
||||||
|
|
||||||
|
<!-- Favicon --> |
||||||
|
<link rel="shortcut icon" href="img/favicon.ico"> |
||||||
|
<!-- favicon ends --> |
||||||
|
|
||||||
|
<!--- LOAD FILES --> |
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> |
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css"> |
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome-animation/0.0.8/font-awesome-animation.min.css"> |
||||||
|
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script> |
||||||
|
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> |
||||||
|
|
||||||
|
<!-- Custom styles --> |
||||||
|
<link rel="stylesheet" href="css/comm.css"> |
||||||
|
|
||||||
|
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> |
||||||
|
<!-- WARNING: Respond.js doesn't work if you view the page via file:// --> |
||||||
|
<!--[if lt IE 9]> |
||||||
|
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.3/html5shiv.js"></script> |
||||||
|
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script> |
||||||
|
<![endif]--> |
||||||
|
</head> |
||||||
|
|
||||||
|
|
||||||
|
<body> |
||||||
|
<div class="container-fluid"> |
||||||
|
<div class="row"> |
||||||
|
<!-- Remote Video --> |
||||||
|
<video id="peerVid" poster="img/vidbg.png" playsinline autoplay></video> |
||||||
|
<!-- Remote Video --> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="row margin-top-20"> |
||||||
|
<!-- Call Buttons --> |
||||||
|
<div class="col-sm-12 text-center" id="callBtns"> |
||||||
|
<button class="btn btn-success btn-sm initCall" id="initAudio"><i class="fa fa-phone"></i></button> |
||||||
|
<button class="btn btn-info btn-sm initCall" id="initVideo"><i class="fa fa-video-camera"></i></button> |
||||||
|
<button class="btn btn-danger btn-sm" id="terminateCall" disabled><i class="fa fa-phone-square"></i></button> |
||||||
|
</div> |
||||||
|
<!-- Call Buttons --> |
||||||
|
|
||||||
|
<!-- Timer --> |
||||||
|
<div class="col-sm-12 text-center margin-top-5" style="color:#fff"> |
||||||
|
<span id="countHr"></span>h: |
||||||
|
<span id="countMin"></span>m: |
||||||
|
<span id="countSec"></span>s |
||||||
|
</div> |
||||||
|
<!-- Timer --> |
||||||
|
</div> |
||||||
|
|
||||||
|
|
||||||
|
<!-- Local Video --> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-sm-12"> |
||||||
|
<video id="myVid" poster="img/vidbg.png" muted autoplay></video> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<!-- Local Video --> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="container-fluid chat-pane"> |
||||||
|
<!-- CHAT PANEL--> |
||||||
|
<div class="row chat-window col-xs-12 col-md-4"> |
||||||
|
<div class=""> |
||||||
|
<div class="panel panel-default chat-pane-panel"> |
||||||
|
<div class="panel-heading chat-pane-top-bar"> |
||||||
|
<div class="col-xs-10" style="margin-left:-20px"> |
||||||
|
<i class="fa fa-comment" id="remoteStatus"></i> Remote |
||||||
|
<b id="remoteStatusTxt">(Offline)</b> |
||||||
|
</div> |
||||||
|
<div class="col-xs-2 pull-right"> |
||||||
|
<span id="minim_chat_window" class="panel-collapsed fa fa-plus icon_minim pointer"></span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="panel-body msg_container_base" id="chats"></div> |
||||||
|
|
||||||
|
<div class="panel-footer"> |
||||||
|
<span id="typingInfo"></span> |
||||||
|
<div class="input-group"> |
||||||
|
<input id="chatInput" type="text" class="form-control input-sm chat_input" placeholder="Type message here..."> |
||||||
|
<span class="input-group-btn"> |
||||||
|
<button class="btn btn-primary btn-sm" id="chatSendBtn">Send</button> |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<!-- CHAT PANEL --> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!--Modal to show that we are calling--> |
||||||
|
<div id="callModal" class="modal fade" role="dialog" data-backdrop="static"> |
||||||
|
<div class="modal-dialog"> |
||||||
|
<div class="modal-content text-center"> |
||||||
|
<div class="modal-header" id="callerInfo"></div> |
||||||
|
|
||||||
|
<div class="modal-body"> |
||||||
|
<button type="button" class="btn btn-danger btn-sm" id='endCall'> |
||||||
|
<i class="fa fa-times-circle"></i> End Call |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<!--Modal end--> |
||||||
|
|
||||||
|
|
||||||
|
<!--Modal to give options to receive call--> |
||||||
|
<div id="rcivModal" class="modal fade" role="dialog" data-backdrop="static"> |
||||||
|
<div class="modal-dialog"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header" id="calleeInfo"></div> |
||||||
|
|
||||||
|
<div class="modal-body text-center"> |
||||||
|
<button type="button" class="btn btn-success btn-sm answerCall" id='startAudio'> |
||||||
|
<i class="fa fa-phone"></i> Audio Call |
||||||
|
</button> |
||||||
|
<button type="button" class="btn btn-success btn-sm answerCall" id='startVideo'> |
||||||
|
<i class="fa fa-video-camera"></i> Video Call |
||||||
|
</button> |
||||||
|
<button type="button" class="btn btn-danger btn-sm" id='rejectCall'> |
||||||
|
<i class="fa fa-times-circle"></i> Reject Call |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<!--Modal end--> |
||||||
|
|
||||||
|
<!--Modal to show flash message--> |
||||||
|
<div id="flashMsgModal" class="modal fade" role="dialog" data-backdrop="static"> |
||||||
|
<div class="modal-dialog"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header" id="flashMsgHeader"> |
||||||
|
<button type="button" class="close" id='closeFlashMsg' data-dismiss="modal">×</button> |
||||||
|
<center><i id="flashMsgIcon"></i> <font id="flashMsg"></font></center> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<!--Modal end--> |
||||||
|
|
||||||
|
<!--Snackbar --> |
||||||
|
<div id="snackbar"></div> |
||||||
|
<!-- Snackbar --> |
||||||
|
|
||||||
|
<!-- custom js --> |
||||||
|
<script> |
||||||
|
var room = "<?=filter_input(INPUT_GET, 'room')?>"; |
||||||
|
var wsChat = new WebSocket("<?=WS_URL?>"); |
||||||
|
</script> |
||||||
|
<script src="js/adapter.js"></script> |
||||||
|
<script src="js/comm.js"></script> |
||||||
|
<audio id="callerTone" src="media/callertone.mp3" loop preload="auto"></audio> |
||||||
|
<audio id="msgTone" src="media/msgtone.mp3" preload="auto"></audio> |
||||||
|
</body> |
||||||
|
</html> |
@ -0,0 +1,232 @@ |
|||||||
|
/* |
||||||
|
Created on : 22-Dec-2016 |
||||||
|
Author : Amir <amirsanni@gmail.com> |
||||||
|
*/ |
||||||
|
|
||||||
|
.margin-top-20{ |
||||||
|
margin-top: 20px; |
||||||
|
} |
||||||
|
|
||||||
|
.pointer{ |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
/* CHAT PANE */ |
||||||
|
#typingInfo{ |
||||||
|
font-size: 10px; |
||||||
|
color: #3a87ad; |
||||||
|
} |
||||||
|
|
||||||
|
.col-md-4, .col-md-10{ |
||||||
|
padding:0; |
||||||
|
} |
||||||
|
.chat-pane-panel{ |
||||||
|
margin-bottom: 0px; |
||||||
|
} |
||||||
|
.chat-window{ |
||||||
|
bottom:0; |
||||||
|
position:fixed; |
||||||
|
} |
||||||
|
.chat-pane-panel{ |
||||||
|
border-radius: 5px 5px 0 0; |
||||||
|
} |
||||||
|
.icon_minim{ |
||||||
|
padding:2px 10px; |
||||||
|
} |
||||||
|
.msg_container_base{ |
||||||
|
background: #e5e5e5; |
||||||
|
margin: 0; |
||||||
|
padding: 0 10px 10px; |
||||||
|
max-height:300px; |
||||||
|
overflow-x:hidden; |
||||||
|
display: none; |
||||||
|
} |
||||||
|
.chat-pane-top-bar { |
||||||
|
background: #666; |
||||||
|
color: white; |
||||||
|
position: relative; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
.msg_receive{ |
||||||
|
padding-left:0; |
||||||
|
margin-left:0; |
||||||
|
} |
||||||
|
.msg_sent{ |
||||||
|
padding-bottom:20px !important; |
||||||
|
margin-right:0; |
||||||
|
} |
||||||
|
.messages { |
||||||
|
background: white; |
||||||
|
padding: 10px; |
||||||
|
border-radius: 2px; |
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); |
||||||
|
max-width:100%; |
||||||
|
} |
||||||
|
.messages > p { |
||||||
|
font-size: 13px; |
||||||
|
margin: 0 0 0.2rem 0; |
||||||
|
word-wrap: break-word |
||||||
|
} |
||||||
|
.messages > time { |
||||||
|
font-size: 11px; |
||||||
|
color: #ccc; |
||||||
|
} |
||||||
|
.msg_container { |
||||||
|
padding: 10px; |
||||||
|
overflow: hidden; |
||||||
|
display: flex; |
||||||
|
} |
||||||
|
.chat-pane-img { |
||||||
|
display: block; |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
.chat-avatar { |
||||||
|
position: relative; |
||||||
|
} |
||||||
|
.base_receive > .chat-avatar:after { |
||||||
|
content: ""; |
||||||
|
position: absolute; |
||||||
|
top: 0; |
||||||
|
right: 0; |
||||||
|
width: 0; |
||||||
|
height: 0; |
||||||
|
border: 5px solid #FFF; |
||||||
|
border-left-color: rgba(0, 0, 0, 0); |
||||||
|
border-bottom-color: rgba(0, 0, 0, 0); |
||||||
|
} |
||||||
|
|
||||||
|
.base_sent { |
||||||
|
justify-content: flex-end; |
||||||
|
align-items: flex-end; |
||||||
|
} |
||||||
|
.base_sent > .chat-avatar:after { |
||||||
|
content: ""; |
||||||
|
position: absolute; |
||||||
|
bottom: 0; |
||||||
|
left: 0; |
||||||
|
width: 0; |
||||||
|
height: 0; |
||||||
|
border: 5px solid white; |
||||||
|
border-right-color: transparent; |
||||||
|
border-top-color: transparent; |
||||||
|
box-shadow: 1px 1px 2px rgba(black, 0.2); /*not quite perfect but close*/ |
||||||
|
} |
||||||
|
|
||||||
|
.msg_sent > time{ |
||||||
|
float: right; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.msg_container_base::-webkit-scrollbar-track |
||||||
|
{ |
||||||
|
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); |
||||||
|
background-color: #F5F5F5; |
||||||
|
} |
||||||
|
|
||||||
|
.msg_container_base::-webkit-scrollbar |
||||||
|
{ |
||||||
|
width: 12px; |
||||||
|
background-color: #F5F5F5; |
||||||
|
} |
||||||
|
|
||||||
|
.msg_container_base::-webkit-scrollbar-thumb |
||||||
|
{ |
||||||
|
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); |
||||||
|
background-color: #555; |
||||||
|
} |
||||||
|
/* _CHAT PANE */ |
||||||
|
|
||||||
|
/* |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
*/ |
||||||
|
|
||||||
|
/* VIDEO */ |
||||||
|
#peerVid{ |
||||||
|
position: fixed; |
||||||
|
top: 50%; |
||||||
|
left: 50%; |
||||||
|
min-width: 100%; |
||||||
|
min-height: 100%; |
||||||
|
width: auto; |
||||||
|
height: auto; |
||||||
|
z-index: -100; |
||||||
|
-ms-transform: translateX(-50%) translateY(-50%); |
||||||
|
-moz-transform: translateX(-50%) translateY(-50%); |
||||||
|
-webkit-transform: translateX(-50%) translateY(-50%); |
||||||
|
transform: translateX(-50%) translateY(-50%); |
||||||
|
background: url(../img/vidbg.png) no-repeat; |
||||||
|
background-size: cover; |
||||||
|
} |
||||||
|
|
||||||
|
#myVid{ |
||||||
|
width: 300px; |
||||||
|
height: 200px; |
||||||
|
bottom: 0; |
||||||
|
position: fixed; |
||||||
|
right: 0; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/* Small screens */ |
||||||
|
@media(max-width:768px){ |
||||||
|
#myVid{ |
||||||
|
width: 200px; |
||||||
|
height: 100px; |
||||||
|
bottom: 100px; |
||||||
|
position: fixed; |
||||||
|
left: 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* _VIDEO */ |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* SNACKBAR */ |
||||||
|
/* The snackbar - position it at the bottom and in the middle of the screen */ |
||||||
|
#snackbar { |
||||||
|
visibility: hidden; /* Hidden by default. Visible on click */ |
||||||
|
min-width: 250px; /* Set a default minimum width */ |
||||||
|
margin-left: -125px; /* Divide value of min-width by 2 */ |
||||||
|
background-color: rgba(158,31,99,0.9); /* background color */ |
||||||
|
color: #fff; /* White text color */ |
||||||
|
text-align: center; /* Centered text */ |
||||||
|
border-radius: 2px; /* Rounded borders */ |
||||||
|
padding: 16px; /* Padding */ |
||||||
|
position: fixed; /* Sit on top of the screen */ |
||||||
|
z-index: 1; /* Add a z-index if needed */ |
||||||
|
right: 0; /* Right the snackbar */ |
||||||
|
top: 90px; /* 90px from the top */ |
||||||
|
} |
||||||
|
|
||||||
|
/* Show the snackbar when clicking on a button (class added with JavaScript) */ |
||||||
|
#snackbar.show { |
||||||
|
visibility: visible; /* Show the snackbar */ |
||||||
|
} |
||||||
|
|
||||||
|
/* Animations to fade the snackbar in and out */ |
||||||
|
@-webkit-keyframes fadein { |
||||||
|
from {right: 0; opacity: 0;} |
||||||
|
to {top: 90px; opacity: 1;} |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes fadein { |
||||||
|
from {right: 0; opacity: 0;} |
||||||
|
to {top: 90px; opacity: 1;} |
||||||
|
} |
||||||
|
|
||||||
|
@-webkit-keyframes fadeout { |
||||||
|
from {top: 90px; opacity: 1;} |
||||||
|
to {right: 0; opacity: 0;} |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes fadeout { |
||||||
|
from {top: 90px; opacity: 1;} |
||||||
|
to {right: 0; opacity: 0;} |
||||||
|
} |
||||||
|
/* _SNACKBAR */ |
After Width: | Height: | Size: 361 KiB |
After Width: | Height: | Size: 4.3 KiB |
@ -0,0 +1,34 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* @fileName index.php |
||||||
|
* @author Amir <amirsanni@gmail.com> |
||||||
|
* @date 22-Dec-2016 |
||||||
|
*/ |
||||||
|
?> |
||||||
|
|
||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"> |
||||||
|
|
||||||
|
<title>Chat App</title> |
||||||
|
|
||||||
|
<!-- Favicon --> |
||||||
|
<link rel="shortcut icon" href="img/favicon.ico"> |
||||||
|
<!-- favicon ends --> |
||||||
|
|
||||||
|
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> |
||||||
|
<!-- WARNING: Respond.js doesn't work if you view the page via file:// --> |
||||||
|
<!--[if lt IE 9]> |
||||||
|
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.3/html5shiv.js"></script> |
||||||
|
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script> |
||||||
|
<![endif]--> |
||||||
|
</head> |
||||||
|
|
||||||
|
|
||||||
|
<body> |
||||||
|
<input type="button" id='createRoom'> |
||||||
|
</body> |
||||||
|
</html> |
Binary file not shown.
Binary file not shown.
@ -0,0 +1,195 @@ |
|||||||
|
<?php |
||||||
|
namespace Amir; |
||||||
|
|
||||||
|
/** |
||||||
|
* Description of Comm |
||||||
|
* |
||||||
|
* @author Amir <amirsanni@gmail.com> |
||||||
|
* @date 26-Oct-2016 |
||||||
|
*/ |
||||||
|
|
||||||
|
|
||||||
|
use Ratchet\MessageComponentInterface; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
|
||||||
|
class Comm implements MessageComponentInterface { |
||||||
|
protected $clients; |
||||||
|
private $rooms; |
||||||
|
|
||||||
|
public function __construct() { |
||||||
|
$this->clients = new \SplObjectStorage; |
||||||
|
$this->rooms = []; |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* |
||||||
|
* @param ConnectionInterface $conn |
||||||
|
*/ |
||||||
|
public function onOpen(ConnectionInterface $conn) { |
||||||
|
// Store the new connection |
||||||
|
$this->clients->attach($conn); |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* |
||||||
|
* @param ConnectionInterface $from |
||||||
|
* @param type $msg |
||||||
|
*/ |
||||||
|
public function onMessage(ConnectionInterface $from, $msg) { |
||||||
|
$data = json_decode($msg); |
||||||
|
$action = $data->action; |
||||||
|
$room = isset($data->room) ? $data->room : ""; |
||||||
|
|
||||||
|
if(($action == 'subscribe') && $room){ |
||||||
|
//subscribe user to room only if he hasn't subscribed |
||||||
|
|
||||||
|
//if room exist and user is yet to subscribe, then subscibe him to room |
||||||
|
//OR |
||||||
|
//if room does not exist, create it by adding user to it |
||||||
|
if((array_key_exists($room, $this->rooms) && !in_array($from, $this->rooms[$room])) || !array_key_exists($room, $this->rooms)){ |
||||||
|
if(isset($this->rooms[$room]) && count($this->rooms[$room]) >= 2){ |
||||||
|
//maximum number of connection reached |
||||||
|
$msg_to_send = json_encode(['action'=>'subRejected']); |
||||||
|
|
||||||
|
$from->send($msg_to_send); |
||||||
|
} |
||||||
|
|
||||||
|
else{ |
||||||
|
$this->rooms[$room][] = $from;//subscribe user to room |
||||||
|
|
||||||
|
$this->notifyUsersOfConnection($room, $from); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
else{ |
||||||
|
//tell user he has subscribed on another device/browser |
||||||
|
$msg_to_send = json_encode(['action'=>'subRejected']); |
||||||
|
|
||||||
|
$from->send($msg_to_send); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
//for other actions |
||||||
|
else if($room && isset($this->rooms[$room])){ |
||||||
|
//send to everybody subscribed to the room received except the sender |
||||||
|
foreach($this->rooms[$room] as $client){ |
||||||
|
if ($client !== $from) { |
||||||
|
$client->send($msg); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* |
||||||
|
* @param ConnectionInterface $conn |
||||||
|
*/ |
||||||
|
public function onClose(ConnectionInterface $conn) { |
||||||
|
// The connection is closed, remove connection |
||||||
|
$this->clients->detach($conn); |
||||||
|
|
||||||
|
if(count($this->rooms)){//if there is at least one room created |
||||||
|
foreach($this->rooms as $room=>$arr_of_subscribers){//loop through the rooms |
||||||
|
foreach ($arr_of_subscribers as $key=>$ratchet_conn){//loop through the users connected to each room |
||||||
|
if($ratchet_conn == $conn){//if the disconnecting user subscribed to this room |
||||||
|
unset($this->rooms[$room][$key]);//remove him from the room |
||||||
|
|
||||||
|
//notify other subscribers that he has disconnected |
||||||
|
$this->notifyUsersOfDisconnection($room, $conn); |
||||||
|
|
||||||
|
echo 'user left'; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* |
||||||
|
* @param ConnectionInterface $conn |
||||||
|
* @param \Exception $e |
||||||
|
*/ |
||||||
|
public function onError(ConnectionInterface $conn, \Exception $e) { |
||||||
|
//echo "An error has occurred: {$e->getMessage()}\n"; |
||||||
|
|
||||||
|
$conn->close(); |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* |
||||||
|
* @param type $room |
||||||
|
* @param type $from |
||||||
|
*/ |
||||||
|
private function notifyUsersOfConnection($room, $from){ |
||||||
|
|
||||||
|
//echo "User subscribed to room ".$room ."\n"; |
||||||
|
|
||||||
|
$msg_to_broadcast = json_encode(['action'=>'newSub', 'room'=>$room]); |
||||||
|
|
||||||
|
//notify user that someone has joined room |
||||||
|
foreach($this->rooms[$room] as $client){ |
||||||
|
if ($client !== $from) { |
||||||
|
$client->send($msg_to_broadcast); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
******************************************************************************************************************************** |
||||||
|
*/ |
||||||
|
|
||||||
|
private function notifyUsersOfDisconnection($room, $from){ |
||||||
|
$msg_to_broadcast = json_encode(['action'=>'imOffline', 'room'=>$room]); |
||||||
|
|
||||||
|
//notify user that remote has left the room |
||||||
|
foreach($this->rooms[$room] as $client){ |
||||||
|
if ($client !== $from) { |
||||||
|
$client->send($msg_to_broadcast); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,25 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
/** |
||||||
|
* Description of server |
||||||
|
* |
||||||
|
* @author Amir <amirsanni@gmail.com> |
||||||
|
* @date 23-Dec-2016 |
||||||
|
*/ |
||||||
|
|
||||||
|
require '../vendor/autoload.php'; |
||||||
|
|
||||||
|
use Amir\Comm; |
||||||
|
|
||||||
|
//set an array of origins allowed to connect to this server |
||||||
|
$allowed_origins = ['localhost', '127.0.0.1']; |
||||||
|
|
||||||
|
// Run the server application through the WebSocket protocol on port 8080 |
||||||
|
$app = new Ratchet\App('localhost', 8080, '0.0.0.0');//App(hostname, port, 'whoCanConnectIP', '') |
||||||
|
|
||||||
|
//create socket routes |
||||||
|
//route(uri, classInstance, arrOfAllowedOrigins) |
||||||
|
$app->route('/comm', new Comm, $allowed_origins); |
||||||
|
|
||||||
|
//run websocket |
||||||
|
$app->run(); |
@ -0,0 +1,11 @@ |
|||||||
|
{ |
||||||
|
"require":{ |
||||||
|
"cboden/ratchet" : "0.3.*" |
||||||
|
}, |
||||||
|
|
||||||
|
"autoload": { |
||||||
|
"psr-4": { |
||||||
|
"Amir\\": "bin/amir/" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,691 @@ |
|||||||
|
{ |
||||||
|
"_readme": [ |
||||||
|
"This file locks the dependencies of your project to a known state", |
||||||
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", |
||||||
|
"This file is @generated automatically" |
||||||
|
], |
||||||
|
"hash": "762037d3d8d94aec831d234f71791628", |
||||||
|
"content-hash": "858ccf2b00629904fbfcaf8568d3cb4b", |
||||||
|
"packages": [ |
||||||
|
{ |
||||||
|
"name": "cboden/ratchet", |
||||||
|
"version": "v0.3.5", |
||||||
|
"source": { |
||||||
|
"type": "git", |
||||||
|
"url": "https://github.com/ratchetphp/Ratchet.git", |
||||||
|
"reference": "b5ccecad9390db85d2c8df7cbeb047292fbbf4b8" |
||||||
|
}, |
||||||
|
"dist": { |
||||||
|
"type": "zip", |
||||||
|
"url": "https://api.github.com/repos/ratchetphp/Ratchet/zipball/b5ccecad9390db85d2c8df7cbeb047292fbbf4b8", |
||||||
|
"reference": "b5ccecad9390db85d2c8df7cbeb047292fbbf4b8", |
||||||
|
"shasum": "" |
||||||
|
}, |
||||||
|
"require": { |
||||||
|
"guzzle/http": "^3.6", |
||||||
|
"php": ">=5.3.9", |
||||||
|
"react/socket": "^0.3 || ^0.4", |
||||||
|
"symfony/http-foundation": "^2.2|^3.0", |
||||||
|
"symfony/routing": "^2.2|^3.0" |
||||||
|
}, |
||||||
|
"type": "library", |
||||||
|
"autoload": { |
||||||
|
"psr-4": { |
||||||
|
"Ratchet\\": "src/Ratchet" |
||||||
|
} |
||||||
|
}, |
||||||
|
"notification-url": "https://packagist.org/downloads/", |
||||||
|
"license": [ |
||||||
|
"MIT" |
||||||
|
], |
||||||
|
"authors": [ |
||||||
|
{ |
||||||
|
"name": "Chris Boden", |
||||||
|
"email": "cboden@gmail.com", |
||||||
|
"role": "Developer" |
||||||
|
} |
||||||
|
], |
||||||
|
"description": "PHP WebSocket library", |
||||||
|
"homepage": "http://socketo.me", |
||||||
|
"keywords": [ |
||||||
|
"Ratchet", |
||||||
|
"WebSockets", |
||||||
|
"server", |
||||||
|
"sockets" |
||||||
|
], |
||||||
|
"time": "2016-05-25 12:55:03" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "evenement/evenement", |
||||||
|
"version": "v2.0.0", |
||||||
|
"source": { |
||||||
|
"type": "git", |
||||||
|
"url": "https://github.com/igorw/evenement.git", |
||||||
|
"reference": "f6e843799fd4f4184d54d8fc7b5b3551c9fa803e" |
||||||
|
}, |
||||||
|
"dist": { |
||||||
|
"type": "zip", |
||||||
|
"url": "https://api.github.com/repos/igorw/evenement/zipball/f6e843799fd4f4184d54d8fc7b5b3551c9fa803e", |
||||||
|
"reference": "f6e843799fd4f4184d54d8fc7b5b3551c9fa803e", |
||||||
|
"shasum": "" |
||||||
|
}, |
||||||
|
"require": { |
||||||
|
"php": ">=5.4.0" |
||||||
|
}, |
||||||
|
"type": "library", |
||||||
|
"extra": { |
||||||
|
"branch-alias": { |
||||||
|
"dev-master": "2.0-dev" |
||||||
|
} |
||||||
|
}, |
||||||
|
"autoload": { |
||||||
|
"psr-0": { |
||||||
|
"Evenement": "src" |
||||||
|
} |
||||||
|
}, |
||||||
|
"notification-url": "https://packagist.org/downloads/", |
||||||
|
"license": [ |
||||||
|
"MIT" |
||||||
|
], |
||||||
|
"authors": [ |
||||||
|
{ |
||||||
|
"name": "Igor Wiedler", |
||||||
|
"email": "igor@wiedler.ch", |
||||||
|
"homepage": "http://wiedler.ch/igor/" |
||||||
|
} |
||||||
|
], |
||||||
|
"description": "Événement is a very simple event dispatching library for PHP", |
||||||
|
"keywords": [ |
||||||
|
"event-dispatcher", |
||||||
|
"event-emitter" |
||||||
|
], |
||||||
|
"time": "2012-11-02 14:49:47" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "guzzle/common", |
||||||
|
"version": "v3.9.2", |
||||||
|
"target-dir": "Guzzle/Common", |
||||||
|
"source": { |
||||||
|
"type": "git", |
||||||
|
"url": "https://github.com/Guzzle3/common.git", |
||||||
|
"reference": "2e36af7cf2ce3ea1f2d7c2831843b883a8e7b7dc" |
||||||
|
}, |
||||||
|
"dist": { |
||||||
|
"type": "zip", |
||||||
|
"url": "https://api.github.com/repos/Guzzle3/common/zipball/2e36af7cf2ce3ea1f2d7c2831843b883a8e7b7dc", |
||||||
|
"reference": "2e36af7cf2ce3ea1f2d7c2831843b883a8e7b7dc", |
||||||
|
"shasum": "" |
||||||
|
}, |
||||||
|
"require": { |
||||||
|
"php": ">=5.3.2", |
||||||
|
"symfony/event-dispatcher": ">=2.1" |
||||||
|
}, |
||||||
|
"type": "library", |
||||||
|
"extra": { |
||||||
|
"branch-alias": { |
||||||
|
"dev-master": "3.7-dev" |
||||||
|
} |
||||||
|
}, |
||||||
|
"autoload": { |
||||||
|
"psr-0": { |
||||||
|
"Guzzle\\Common": "" |
||||||
|
} |
||||||
|
}, |
||||||
|
"notification-url": "https://packagist.org/downloads/", |
||||||
|
"license": [ |
||||||
|
"MIT" |
||||||
|
], |
||||||
|
"description": "Common libraries used by Guzzle", |
||||||
|
"homepage": "http://guzzlephp.org/", |
||||||
|
"keywords": [ |
||||||
|
"collection", |
||||||
|
"common", |
||||||
|
"event", |
||||||
|
"exception" |
||||||
|
], |
||||||
|
"abandoned": "guzzle/guzzle", |
||||||
|
"time": "2014-08-11 04:32:36" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "guzzle/http", |
||||||
|
"version": "v3.9.2", |
||||||
|
"target-dir": "Guzzle/Http", |
||||||
|
"source": { |
||||||
|
"type": "git", |
||||||
|
"url": "https://github.com/Guzzle3/http.git", |
||||||
|
"reference": "1e8dd1e2ba9dc42332396f39fbfab950b2301dc5" |
||||||
|
}, |
||||||
|
"dist": { |
||||||
|
"type": "zip", |
||||||
|
"url": "https://api.github.com/repos/Guzzle3/http/zipball/1e8dd1e2ba9dc42332396f39fbfab950b2301dc5", |
||||||
|
"reference": "1e8dd1e2ba9dc42332396f39fbfab950b2301dc5", |
||||||
|
"shasum": "" |
||||||
|
}, |
||||||
|
"require": { |
||||||
|
"guzzle/common": "self.version", |
||||||
|
"guzzle/parser": "self.version", |
||||||
|
"guzzle/stream": "self.version", |
||||||
|
"php": ">=5.3.2" |
||||||
|
}, |
||||||
|
"suggest": { |
||||||
|
"ext-curl": "*" |
||||||
|
}, |
||||||
|
"type": "library", |
||||||
|
"extra": { |
||||||
|
"branch-alias": { |
||||||
|
"dev-master": "3.7-dev" |
||||||
|
} |
||||||
|
}, |
||||||
|
"autoload": { |
||||||
|
"psr-0": { |
||||||
|
"Guzzle\\Http": "" |
||||||
|
} |
||||||
|
}, |
||||||
|
"notification-url": "https://packagist.org/downloads/", |
||||||
|
"license": [ |
||||||
|
"MIT" |
||||||
|
], |
||||||
|
"authors": [ |
||||||
|
{ |
||||||
|
"name": "Michael Dowling", |
||||||
|
"email": "mtdowling@gmail.com", |
||||||
|
"homepage": "https://github.com/mtdowling" |
||||||
|
} |
||||||
|
], |
||||||
|
"description": "HTTP libraries used by Guzzle", |
||||||
|
"homepage": "http://guzzlephp.org/", |
||||||
|
"keywords": [ |
||||||
|
"Guzzle", |
||||||
|
"client", |
||||||
|
"curl", |
||||||
|
"http", |
||||||
|
"http client" |
||||||
|
], |
||||||
|
"abandoned": "guzzle/guzzle", |
||||||
|
"time": "2014-08-11 04:32:36" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "guzzle/parser", |
||||||
|
"version": "v3.9.2", |
||||||
|
"target-dir": "Guzzle/Parser", |
||||||
|
"source": { |
||||||
|
"type": "git", |
||||||
|
"url": "https://github.com/Guzzle3/parser.git", |
||||||
|
"reference": "6874d171318a8e93eb6d224cf85e4678490b625c" |
||||||
|
}, |
||||||
|
"dist": { |
||||||
|
"type": "zip", |
||||||
|
"url": "https://api.github.com/repos/Guzzle3/parser/zipball/6874d171318a8e93eb6d224cf85e4678490b625c", |
||||||
|
"reference": "6874d171318a8e93eb6d224cf85e4678490b625c", |
||||||
|
"shasum": "" |
||||||
|
}, |
||||||
|
"require": { |
||||||
|
"php": ">=5.3.2" |
||||||
|
}, |
||||||
|
"type": "library", |
||||||
|
"extra": { |
||||||
|
"branch-alias": { |
||||||
|
"dev-master": "3.7-dev" |
||||||
|
} |
||||||
|
}, |
||||||
|
"autoload": { |
||||||
|
"psr-0": { |
||||||
|
"Guzzle\\Parser": "" |
||||||
|
} |
||||||
|
}, |
||||||
|
"notification-url": "https://packagist.org/downloads/", |
||||||
|
"license": [ |
||||||
|
"MIT" |
||||||
|
], |
||||||
|
"description": "Interchangeable parsers used by Guzzle", |
||||||
|
"homepage": "http://guzzlephp.org/", |
||||||
|
"keywords": [ |
||||||
|
"URI Template", |
||||||
|
"cookie", |
||||||
|
"http", |
||||||
|
"message", |
||||||
|
"url" |
||||||
|
], |
||||||
|
"abandoned": "guzzle/guzzle", |
||||||
|
"time": "2014-02-05 18:29:46" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "guzzle/stream", |
||||||
|
"version": "v3.9.2", |
||||||
|
"target-dir": "Guzzle/Stream", |
||||||
|
"source": { |
||||||
|
"type": "git", |
||||||
|
"url": "https://github.com/Guzzle3/stream.git", |
||||||
|
"reference": "60c7fed02e98d2c518dae8f97874c8f4622100f0" |
||||||
|
}, |
||||||
|
"dist": { |
||||||
|
"type": "zip", |
||||||
|
"url": "https://api.github.com/repos/Guzzle3/stream/zipball/60c7fed02e98d2c518dae8f97874c8f4622100f0", |
||||||
|
"reference": "60c7fed02e98d2c518dae8f97874c8f4622100f0", |
||||||
|
"shasum": "" |
||||||
|
}, |
||||||
|
"require": { |
||||||
|
"guzzle/common": "self.version", |
||||||
|
"php": ">=5.3.2" |
||||||
|
}, |
||||||
|
"suggest": { |
||||||
|
"guzzle/http": "To convert Guzzle request objects to PHP streams" |
||||||
|
}, |
||||||
|
"type": "library", |
||||||
|
"extra": { |
||||||
|
"branch-alias": { |
||||||
|
"dev-master": "3.7-dev" |
||||||
|
} |
||||||
|
}, |
||||||
|
"autoload": { |
||||||
|
"psr-0": { |
||||||
|
"Guzzle\\Stream": "" |
||||||
|
} |
||||||
|
}, |
||||||
|
"notification-url": "https://packagist.org/downloads/", |
||||||
|
"license": [ |
||||||
|
"MIT" |
||||||
|
], |
||||||
|
"authors": [ |
||||||
|
{ |
||||||
|
"name": "Michael Dowling", |
||||||
|
"email": "mtdowling@gmail.com", |
||||||
|
"homepage": "https://github.com/mtdowling" |
||||||
|
} |
||||||
|
], |
||||||
|
"description": "Guzzle stream wrapper component", |
||||||
|
"homepage": "http://guzzlephp.org/", |
||||||
|
"keywords": [ |
||||||
|
"Guzzle", |
||||||
|
"component", |
||||||
|
"stream" |
||||||
|
], |
||||||
|
"abandoned": "guzzle/guzzle", |
||||||
|
"time": "2014-05-01 21:36:02" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "react/event-loop", |
||||||
|
"version": "v0.4.2", |
||||||
|
"source": { |
||||||
|
"type": "git", |
||||||
|
"url": "https://github.com/reactphp/event-loop.git", |
||||||
|
"reference": "164799f73175e1c80bba92a220ea35df6ca371dd" |
||||||
|
}, |
||||||
|
"dist": { |
||||||
|
"type": "zip", |
||||||
|
"url": "https://api.github.com/repos/reactphp/event-loop/zipball/164799f73175e1c80bba92a220ea35df6ca371dd", |
||||||
|
"reference": "164799f73175e1c80bba92a220ea35df6ca371dd", |
||||||
|
"shasum": "" |
||||||
|
}, |
||||||
|
"require": { |
||||||
|
"php": ">=5.4.0" |
||||||
|
}, |
||||||
|
"suggest": { |
||||||
|
"ext-event": "~1.0", |
||||||
|
"ext-libev": "*", |
||||||
|
"ext-libevent": ">=0.1.0" |
||||||
|
}, |
||||||
|
"type": "library", |
||||||
|
"extra": { |
||||||
|
"branch-alias": { |
||||||
|
"dev-master": "0.5-dev" |
||||||
|
} |
||||||
|
}, |
||||||
|
"autoload": { |
||||||
|
"psr-4": { |
||||||
|
"React\\EventLoop\\": "src" |
||||||
|
} |
||||||
|
}, |
||||||
|
"notification-url": "https://packagist.org/downloads/", |
||||||
|
"license": [ |
||||||
|
"MIT" |
||||||
|
], |
||||||
|
"description": "Event loop abstraction layer that libraries can use for evented I/O.", |
||||||
|
"keywords": [ |
||||||
|
"asynchronous", |
||||||
|
"event-loop" |
||||||
|
], |
||||||
|
"time": "2016-03-08 02:09:32" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "react/socket", |
||||||
|
"version": "v0.4.3", |
||||||
|
"source": { |
||||||
|
"type": "git", |
||||||
|
"url": "https://github.com/reactphp/socket.git", |
||||||
|
"reference": "ce015ec5879b96f5d30905f035f223aa85013fcc" |
||||||
|
}, |
||||||
|
"dist": { |
||||||
|
"type": "zip", |
||||||
|
"url": "https://api.github.com/repos/reactphp/socket/zipball/ce015ec5879b96f5d30905f035f223aa85013fcc", |
||||||
|
"reference": "ce015ec5879b96f5d30905f035f223aa85013fcc", |
||||||
|
"shasum": "" |
||||||
|
}, |
||||||
|
"require": { |
||||||
|
"evenement/evenement": "~2.0|~1.0", |
||||||
|
"php": ">=5.3.0", |
||||||
|
"react/event-loop": "0.4.*|0.3.*", |
||||||
|
"react/stream": "0.4.*|0.3.*" |
||||||
|
}, |
||||||
|
"type": "library", |
||||||
|
"extra": { |
||||||
|
"branch-alias": { |
||||||
|
"dev-master": "0.4-dev" |
||||||
|
} |
||||||
|
}, |
||||||
|
"autoload": { |
||||||
|
"psr-4": { |
||||||
|
"React\\Socket\\": "src" |
||||||
|
} |
||||||
|
}, |
||||||
|
"notification-url": "https://packagist.org/downloads/", |
||||||
|
"license": [ |
||||||
|
"MIT" |
||||||
|
], |
||||||
|
"description": "Library for building an evented socket server.", |
||||||
|
"keywords": [ |
||||||
|
"Socket" |
||||||
|
], |
||||||
|
"time": "2016-03-01 20:10:35" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "react/stream", |
||||||
|
"version": "v0.4.4", |
||||||
|
"source": { |
||||||
|
"type": "git", |
||||||
|
"url": "https://github.com/reactphp/stream.git", |
||||||
|
"reference": "fcc9e7cea126962cff303c90c05e2efdcec09a27" |
||||||
|
}, |
||||||
|
"dist": { |
||||||
|
"type": "zip", |
||||||
|
"url": "https://api.github.com/repos/reactphp/stream/zipball/fcc9e7cea126962cff303c90c05e2efdcec09a27", |
||||||
|
"reference": "fcc9e7cea126962cff303c90c05e2efdcec09a27", |
||||||
|
"shasum": "" |
||||||
|
}, |
||||||
|
"require": { |
||||||
|
"evenement/evenement": "^2.0|^1.0", |
||||||
|
"php": ">=5.3.8" |
||||||
|
}, |
||||||
|
"require-dev": { |
||||||
|
"clue/stream-filter": "~1.2", |
||||||
|
"react/event-loop": "^0.4|^0.3", |
||||||
|
"react/promise": "^2.0|^1.0" |
||||||
|
}, |
||||||
|
"suggest": { |
||||||
|
"react/event-loop": "^0.4", |
||||||
|
"react/promise": "^2.0" |
||||||
|
}, |
||||||
|
"type": "library", |
||||||
|
"autoload": { |
||||||
|
"psr-4": { |
||||||
|
"React\\Stream\\": "src" |
||||||
|
} |
||||||
|
}, |
||||||
|
"notification-url": "https://packagist.org/downloads/", |
||||||
|
"license": [ |
||||||
|
"MIT" |
||||||
|
], |
||||||
|
"description": "Basic readable and writable stream interfaces that support piping.", |
||||||
|
"keywords": [ |
||||||
|
"pipe", |
||||||
|
"stream" |
||||||
|
], |
||||||
|
"time": "2016-08-22 09:51:07" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "symfony/event-dispatcher", |
||||||
|
"version": "v3.1.6", |
||||||
|
"source": { |
||||||
|
"type": "git", |
||||||
|
"url": "https://github.com/symfony/event-dispatcher.git", |
||||||
|
"reference": "28b0832b2553ffb80cabef6a7a812ff1e670c0bc" |
||||||
|
}, |
||||||
|
"dist": { |
||||||
|
"type": "zip", |
||||||
|
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/28b0832b2553ffb80cabef6a7a812ff1e670c0bc", |
||||||
|
"reference": "28b0832b2553ffb80cabef6a7a812ff1e670c0bc", |
||||||
|
"shasum": "" |
||||||
|
}, |
||||||
|
"require": { |
||||||
|
"php": ">=5.5.9" |
||||||
|
}, |
||||||
|
"require-dev": { |
||||||
|
"psr/log": "~1.0", |
||||||
|
"symfony/config": "~2.8|~3.0", |
||||||
|
"symfony/dependency-injection": "~2.8|~3.0", |
||||||
|
"symfony/expression-language": "~2.8|~3.0", |
||||||
|
"symfony/stopwatch": "~2.8|~3.0" |
||||||
|
}, |
||||||
|
"suggest": { |
||||||
|
"symfony/dependency-injection": "", |
||||||
|
"symfony/http-kernel": "" |
||||||
|
}, |
||||||
|
"type": "library", |
||||||
|
"extra": { |
||||||
|
"branch-alias": { |
||||||
|
"dev-master": "3.1-dev" |
||||||
|
} |
||||||
|
}, |
||||||
|
"autoload": { |
||||||
|
"psr-4": { |
||||||
|
"Symfony\\Component\\EventDispatcher\\": "" |
||||||
|
}, |
||||||
|
"exclude-from-classmap": [ |
||||||
|
"/Tests/" |
||||||
|
] |
||||||
|
}, |
||||||
|
"notification-url": "https://packagist.org/downloads/", |
||||||
|
"license": [ |
||||||
|
"MIT" |
||||||
|
], |
||||||
|
"authors": [ |
||||||
|
{ |
||||||
|
"name": "Fabien Potencier", |
||||||
|
"email": "fabien@symfony.com" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "Symfony Community", |
||||||
|
"homepage": "https://symfony.com/contributors" |
||||||
|
} |
||||||
|
], |
||||||
|
"description": "Symfony EventDispatcher Component", |
||||||
|
"homepage": "https://symfony.com", |
||||||
|
"time": "2016-10-13 06:28:43" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "symfony/http-foundation", |
||||||
|
"version": "v3.1.6", |
||||||
|
"source": { |
||||||
|
"type": "git", |
||||||
|
"url": "https://github.com/symfony/http-foundation.git", |
||||||
|
"reference": "f21e5a8b88274b7720779aa88f9c02c6d6ec08d7" |
||||||
|
}, |
||||||
|
"dist": { |
||||||
|
"type": "zip", |
||||||
|
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/f21e5a8b88274b7720779aa88f9c02c6d6ec08d7", |
||||||
|
"reference": "f21e5a8b88274b7720779aa88f9c02c6d6ec08d7", |
||||||
|
"shasum": "" |
||||||
|
}, |
||||||
|
"require": { |
||||||
|
"php": ">=5.5.9", |
||||||
|
"symfony/polyfill-mbstring": "~1.1" |
||||||
|
}, |
||||||
|
"require-dev": { |
||||||
|
"symfony/expression-language": "~2.8|~3.0" |
||||||
|
}, |
||||||
|
"type": "library", |
||||||
|
"extra": { |
||||||
|
"branch-alias": { |
||||||
|
"dev-master": "3.1-dev" |
||||||
|
} |
||||||
|
}, |
||||||
|
"autoload": { |
||||||
|
"psr-4": { |
||||||
|
"Symfony\\Component\\HttpFoundation\\": "" |
||||||
|
}, |
||||||
|
"exclude-from-classmap": [ |
||||||
|
"/Tests/" |
||||||
|
] |
||||||
|
}, |
||||||
|
"notification-url": "https://packagist.org/downloads/", |
||||||
|
"license": [ |
||||||
|
"MIT" |
||||||
|
], |
||||||
|
"authors": [ |
||||||
|
{ |
||||||
|
"name": "Fabien Potencier", |
||||||
|
"email": "fabien@symfony.com" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "Symfony Community", |
||||||
|
"homepage": "https://symfony.com/contributors" |
||||||
|
} |
||||||
|
], |
||||||
|
"description": "Symfony HttpFoundation Component", |
||||||
|
"homepage": "https://symfony.com", |
||||||
|
"time": "2016-10-24 15:52:44" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "symfony/polyfill-mbstring", |
||||||
|
"version": "v1.2.0", |
||||||
|
"source": { |
||||||
|
"type": "git", |
||||||
|
"url": "https://github.com/symfony/polyfill-mbstring.git", |
||||||
|
"reference": "dff51f72b0706335131b00a7f49606168c582594" |
||||||
|
}, |
||||||
|
"dist": { |
||||||
|
"type": "zip", |
||||||
|
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/dff51f72b0706335131b00a7f49606168c582594", |
||||||
|
"reference": "dff51f72b0706335131b00a7f49606168c582594", |
||||||
|
"shasum": "" |
||||||
|
}, |
||||||
|
"require": { |
||||||
|
"php": ">=5.3.3" |
||||||
|
}, |
||||||
|
"suggest": { |
||||||
|
"ext-mbstring": "For best performance" |
||||||
|
}, |
||||||
|
"type": "library", |
||||||
|
"extra": { |
||||||
|
"branch-alias": { |
||||||
|
"dev-master": "1.2-dev" |
||||||
|
} |
||||||
|
}, |
||||||
|
"autoload": { |
||||||
|
"psr-4": { |
||||||
|
"Symfony\\Polyfill\\Mbstring\\": "" |
||||||
|
}, |
||||||
|
"files": [ |
||||||
|
"bootstrap.php" |
||||||
|
] |
||||||
|
}, |
||||||
|
"notification-url": "https://packagist.org/downloads/", |
||||||
|
"license": [ |
||||||
|
"MIT" |
||||||
|
], |
||||||
|
"authors": [ |
||||||
|
{ |
||||||
|
"name": "Nicolas Grekas", |
||||||
|
"email": "p@tchwork.com" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "Symfony Community", |
||||||
|
"homepage": "https://symfony.com/contributors" |
||||||
|
} |
||||||
|
], |
||||||
|
"description": "Symfony polyfill for the Mbstring extension", |
||||||
|
"homepage": "https://symfony.com", |
||||||
|
"keywords": [ |
||||||
|
"compatibility", |
||||||
|
"mbstring", |
||||||
|
"polyfill", |
||||||
|
"portable", |
||||||
|
"shim" |
||||||
|
], |
||||||
|
"time": "2016-05-18 14:26:46" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "symfony/routing", |
||||||
|
"version": "v3.1.6", |
||||||
|
"source": { |
||||||
|
"type": "git", |
||||||
|
"url": "https://github.com/symfony/routing.git", |
||||||
|
"reference": "8edf62498a1a4c57ba317664a4b698339c10cdf6" |
||||||
|
}, |
||||||
|
"dist": { |
||||||
|
"type": "zip", |
||||||
|
"url": "https://api.github.com/repos/symfony/routing/zipball/8edf62498a1a4c57ba317664a4b698339c10cdf6", |
||||||
|
"reference": "8edf62498a1a4c57ba317664a4b698339c10cdf6", |
||||||
|
"shasum": "" |
||||||
|
}, |
||||||
|
"require": { |
||||||
|
"php": ">=5.5.9" |
||||||
|
}, |
||||||
|
"conflict": { |
||||||
|
"symfony/config": "<2.8" |
||||||
|
}, |
||||||
|
"require-dev": { |
||||||
|
"doctrine/annotations": "~1.0", |
||||||
|
"doctrine/common": "~2.2", |
||||||
|
"psr/log": "~1.0", |
||||||
|
"symfony/config": "~2.8|~3.0", |
||||||
|
"symfony/expression-language": "~2.8|~3.0", |
||||||
|
"symfony/http-foundation": "~2.8|~3.0", |
||||||
|
"symfony/yaml": "~2.8|~3.0" |
||||||
|
}, |
||||||
|
"suggest": { |
||||||
|
"doctrine/annotations": "For using the annotation loader", |
||||||
|
"symfony/config": "For using the all-in-one router or any loader", |
||||||
|
"symfony/dependency-injection": "For loading routes from a service", |
||||||
|
"symfony/expression-language": "For using expression matching", |
||||||
|
"symfony/http-foundation": "For using a Symfony Request object", |
||||||
|
"symfony/yaml": "For using the YAML loader" |
||||||
|
}, |
||||||
|
"type": "library", |
||||||
|
"extra": { |
||||||
|
"branch-alias": { |
||||||
|
"dev-master": "3.1-dev" |
||||||
|
} |
||||||
|
}, |
||||||
|
"autoload": { |
||||||
|
"psr-4": { |
||||||
|
"Symfony\\Component\\Routing\\": "" |
||||||
|
}, |
||||||
|
"exclude-from-classmap": [ |
||||||
|
"/Tests/" |
||||||
|
] |
||||||
|
}, |
||||||
|
"notification-url": "https://packagist.org/downloads/", |
||||||
|
"license": [ |
||||||
|
"MIT" |
||||||
|
], |
||||||
|
"authors": [ |
||||||
|
{ |
||||||
|
"name": "Fabien Potencier", |
||||||
|
"email": "fabien@symfony.com" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "Symfony Community", |
||||||
|
"homepage": "https://symfony.com/contributors" |
||||||
|
} |
||||||
|
], |
||||||
|
"description": "Symfony Routing Component", |
||||||
|
"homepage": "https://symfony.com", |
||||||
|
"keywords": [ |
||||||
|
"router", |
||||||
|
"routing", |
||||||
|
"uri", |
||||||
|
"url" |
||||||
|
], |
||||||
|
"time": "2016-08-16 14:58:24" |
||||||
|
} |
||||||
|
], |
||||||
|
"packages-dev": [], |
||||||
|
"aliases": [], |
||||||
|
"minimum-stability": "stable", |
||||||
|
"stability-flags": [], |
||||||
|
"prefer-stable": false, |
||||||
|
"prefer-lowest": false, |
||||||
|
"platform": [], |
||||||
|
"platform-dev": [] |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
// autoload.php @generated by Composer |
||||||
|
|
||||||
|
require_once __DIR__ . '/composer' . '/autoload_real.php'; |
||||||
|
|
||||||
|
return ComposerAutoloaderInita3d708ca3bb0f3f4e817238e4fb57153::getLoader(); |
@ -0,0 +1,5 @@ |
|||||||
|
phpunit.xml |
||||||
|
reports |
||||||
|
sandbox |
||||||
|
vendor |
||||||
|
composer.lock |
@ -0,0 +1,13 @@ |
|||||||
|
language: php |
||||||
|
|
||||||
|
php: |
||||||
|
- 5.3 |
||||||
|
- 5.4 |
||||||
|
- 5.5 |
||||||
|
- 5.6 |
||||||
|
- 7 |
||||||
|
- hhvm |
||||||
|
|
||||||
|
before_script: |
||||||
|
- sh -c 'if [ "$TRAVIS_PHP_VERSION" != "hhvm" ]; then echo "session.serialize_handler = php" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi;' |
||||||
|
- composer install --dev --prefer-source |
@ -0,0 +1,134 @@ |
|||||||
|
CHANGELOG |
||||||
|
========= |
||||||
|
|
||||||
|
###Legend |
||||||
|
|
||||||
|
* "BC": Backwards compatibility break (from public component APIs) |
||||||
|
* "BF": Bug fix |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
* 0.3.5 (2016-05-25) |
||||||
|
|
||||||
|
* BF: Unmask responding close frame |
||||||
|
* Added write handler for PHP session serializer |
||||||
|
|
||||||
|
* 0.3.4 (2015-12-23) |
||||||
|
|
||||||
|
* BF: Edge case where version check wasn't run on message coalesce |
||||||
|
* BF: Session didn't start when using pdo_sqlite |
||||||
|
* BF: WAMP currie prefix check when using '#' |
||||||
|
* Compatibility with Symfony 3 |
||||||
|
|
||||||
|
* 0.3.3 (2015-05-26) |
||||||
|
|
||||||
|
* BF: Framing bug on large messages upon TCP fragmentation |
||||||
|
* BF: Symfony Router query parameter defaults applied to Request |
||||||
|
* BF: WAMP CURIE on all URIs |
||||||
|
* OriginCheck rules applied to FlashPolicy |
||||||
|
* Switched from PSR-0 to PSR-4 |
||||||
|
|
||||||
|
* 0.3.2 (2014-06-08) |
||||||
|
|
||||||
|
* BF: No messages after closing handshake (fixed rare race condition causing 100% CPU) |
||||||
|
* BF: Fixed accidental BC break from v0.3.1 |
||||||
|
* Added autoDelete parameter to Topic to destroy when empty of connections |
||||||
|
* Exposed React Socket on IoServer (allowing FlashPolicy shutdown in App) |
||||||
|
* Normalized Exceptions in WAMP |
||||||
|
|
||||||
|
* 0.3.1 (2014-05-26) |
||||||
|
|
||||||
|
* Added query parameter support to Router, set in HTTP request (ws://server?hello=world) |
||||||
|
* HHVM compatibility |
||||||
|
* BF: React/0.4 support; CPU starvation bug fixes |
||||||
|
* BF: Allow App::route to ignore Host header |
||||||
|
* Added expected filters to WAMP Topic broadcast method |
||||||
|
* Resource cleanup in WAMP TopicManager |
||||||
|
|
||||||
|
* 0.3.0 (2013-10-14) |
||||||
|
|
||||||
|
* Added the `App` class to help making Ratchet so easy to use it's silly |
||||||
|
* BC: Require hostname to do HTTP Host header match and do Origin HTTP header check, verify same name by default, helping prevent CSRF attacks |
||||||
|
* Added Symfony/2.2 based HTTP Router component to allowing for a single Ratchet server to handle multiple apps -> Ratchet\Http\Router |
||||||
|
* BC: Decoupled HTTP from WebSocket component -> Ratchet\Http\HttpServer |
||||||
|
* BF: Single sub-protocol selection to conform with RFC6455 |
||||||
|
* BF: Sanity checks on WAMP protocol to prevent errors |
||||||
|
|
||||||
|
* 0.2.8 (2013-09-19) |
||||||
|
|
||||||
|
* React 0.3 support |
||||||
|
|
||||||
|
* 0.2.7 (2013-06-09) |
||||||
|
|
||||||
|
* BF: Sub-protocol negotation with Guzzle 3.6 |
||||||
|
|
||||||
|
* 0.2.6 (2013-06-01) |
||||||
|
|
||||||
|
* Guzzle 3.6 support |
||||||
|
|
||||||
|
* 0.2.5 (2013-04-01) |
||||||
|
|
||||||
|
* Fixed Hixie-76 handshake bug |
||||||
|
|
||||||
|
* 0.2.4 (2013-03-09) |
||||||
|
|
||||||
|
* Support for Symfony 2.2 and Guzzle 2.3 |
||||||
|
* Minor bug fixes when handling errors |
||||||
|
|
||||||
|
* 0.2.3 (2012-11-21) |
||||||
|
|
||||||
|
* Bumped dep: Guzzle to v3, React to v0.2.4 |
||||||
|
* More tests |
||||||
|
|
||||||
|
* 0.2.2 (2012-10-20) |
||||||
|
|
||||||
|
* Bumped deps to use React v0.2 |
||||||
|
|
||||||
|
* 0.2.1 (2012-10-13) |
||||||
|
|
||||||
|
* BF: No more UTF-8 warnings in browsers (no longer sending empty sub-protocol string) |
||||||
|
* Documentation corrections |
||||||
|
* Using new composer structure |
||||||
|
|
||||||
|
* 0.2 (2012-09-07) |
||||||
|
|
||||||
|
* Ratchet passes every non-binary-frame test from the Autobahn Testsuite |
||||||
|
* Major performance improvements |
||||||
|
* BC: Renamed "WampServer" to "ServerProtocol" |
||||||
|
* BC: New "WampServer" component passes Topic container objects of subscribed Connections |
||||||
|
* Option to turn off UTF-8 checks in order to increase performance |
||||||
|
* Switched dependency guzzle/guzzle to guzzle/http (no API changes) |
||||||
|
* mbstring no longer required |
||||||
|
|
||||||
|
* 0.1.5 (2012-07-12) |
||||||
|
|
||||||
|
* BF: Error where service wouldn't run on PHP <= 5.3.8 |
||||||
|
* Dependency library updates |
||||||
|
|
||||||
|
* 0.1.4 (2012-06-17) |
||||||
|
|
||||||
|
* Fixed dozens of failing AB tests |
||||||
|
* BF: Proper socket buffer handling |
||||||
|
|
||||||
|
* 0.1.3 (2012-06-15) |
||||||
|
|
||||||
|
* Major refactor inside WebSocket protocol handling, more loosley coupled |
||||||
|
* BF: Proper error handling on failed WebSocket connections |
||||||
|
* BF: Handle TCP message concatenation |
||||||
|
* Inclusion of the AutobahnTestSuite checking WebSocket protocol compliance |
||||||
|
* mb_string now a requirement |
||||||
|
|
||||||
|
* 0.1.2 (2012-05-19) |
||||||
|
|
||||||
|
* BC/BF: Updated WAMP API to coincide with the official spec |
||||||
|
* Tweaks to improve running as a long lived process |
||||||
|
|
||||||
|
* 0.1.1 (2012-05-14) |
||||||
|
|
||||||
|
* Separated interfaces allowing WebSockets to support multiple sub protocols |
||||||
|
* BF: remoteAddress variable on connections returns proper value |
||||||
|
|
||||||
|
* 0.1 (2012-05-11) |
||||||
|
|
||||||
|
* First release with components: IoServer, WsServer, SessionProvider, WampServer, FlashPolicy, IpBlackList |
||||||
|
* I/O now handled by React, making Ratchet fully asynchronous |
@ -0,0 +1,19 @@ |
|||||||
|
Copyright (c) 2011-2016 Chris Boden |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished |
||||||
|
to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
||||||
|
THE SOFTWARE. |
@ -0,0 +1,35 @@ |
|||||||
|
# This file is intended to ease the author's development and testing process
|
||||||
|
# Users do not need to use `make`; Ratchet does not need to be compiled
|
||||||
|
|
||||||
|
test: |
||||||
|
phpunit |
||||||
|
|
||||||
|
cover: |
||||||
|
phpunit --coverage-text --coverage-html=reports/coverage |
||||||
|
|
||||||
|
abtests: |
||||||
|
ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8001 LibEvent & |
||||||
|
ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8002 StreamSelect & |
||||||
|
ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver-noutf8.php 8003 StreamSelect & |
||||||
|
ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8004 LibEv & |
||||||
|
wstest -m testeeserver -w ws://localhost:8000 & |
||||||
|
wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-all.json |
||||||
|
killall php wstest |
||||||
|
|
||||||
|
abtest: |
||||||
|
ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8000 StreamSelect & |
||||||
|
wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-quick.json |
||||||
|
killall php |
||||||
|
|
||||||
|
profile: |
||||||
|
php -d 'xdebug.profiler_enable=1' tests/autobahn/bin/fuzzingserver.php 8000 LibEvent & |
||||||
|
wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-profile.json |
||||||
|
killall php |
||||||
|
|
||||||
|
apidocs: |
||||||
|
apigen --title Ratchet -d reports/api -s src/ \
|
||||||
|
-s vendor/react \
|
||||||
|
-s vendor/guzzle \
|
||||||
|
-s vendor/symfony/http-foundation/Symfony/Component/HttpFoundation/Session \
|
||||||
|
-s vendor/symfony/routing/Symfony/Component/Routing \
|
||||||
|
-s vendor/evenement/evenement/src/Evenement |
@ -0,0 +1,90 @@ |
|||||||
|
#Ratchet |
||||||
|
|
||||||
|
[](http://travis-ci.org/ratchetphp/Ratchet) |
||||||
|
[](https://packagist.org/packages/cboden/ratchet) |
||||||
|
|
||||||
|
A PHP 5.3 library for asynchronously serving WebSockets. |
||||||
|
Build up your application through simple interfaces and re-use your application without changing any of its code just by combining different components. |
||||||
|
|
||||||
|
##WebSocket Compliance |
||||||
|
|
||||||
|
* Supports the RFC6455, HyBi-10+, and Hixie76 protocol versions (at the same time) |
||||||
|
* Tested on Chrome 13+, Firefox 6+, Safari 5+, iOS 4.2+, IE 8+ |
||||||
|
* Ratchet [passes](http://socketo.me/reports/ab/) the [Autobahn Testsuite](http://autobahn.ws/testsuite) (non-binary messages) |
||||||
|
|
||||||
|
##Requirements |
||||||
|
|
||||||
|
Shell access is required and root access is recommended. |
||||||
|
To avoid proxy/firewall blockage it's recommended WebSockets are requested on port 80 or 443 (SSL), which requires root access. |
||||||
|
In order to do this, along with your sync web stack, you can either use a reverse proxy or two separate machines. |
||||||
|
You can find more details in the [server conf docs](http://socketo.me/docs/deploy#serverconfiguration). |
||||||
|
|
||||||
|
PHP 5.3.9 (or higher) is required. If you have access, PHP 5.4 (or higher) is *highly* recommended for its performance improvements. |
||||||
|
|
||||||
|
### Documentation |
||||||
|
|
||||||
|
User and API documentation is available on Ratchet's website: http://socketo.me |
||||||
|
|
||||||
|
See https://github.com/cboden/Ratchet-examples for some out-of-the-box working demos using Ratchet. |
||||||
|
|
||||||
|
Need help? Have a question? Want to provide feedback? Write a message on the [Google Groups Mailing List](https://groups.google.com/forum/#!forum/ratchet-php). |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
###A quick example |
||||||
|
|
||||||
|
```php |
||||||
|
<?php |
||||||
|
use Ratchet\MessageComponentInterface; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
|
||||||
|
// Make sure composer dependencies have been installed |
||||||
|
require __DIR__ . '/vendor/autoload.php'; |
||||||
|
|
||||||
|
/** |
||||||
|
* chat.php |
||||||
|
* Send any incoming messages to all connected clients (except sender) |
||||||
|
*/ |
||||||
|
class MyChat implements MessageComponentInterface { |
||||||
|
protected $clients; |
||||||
|
|
||||||
|
public function __construct() { |
||||||
|
$this->clients = new \SplObjectStorage; |
||||||
|
} |
||||||
|
|
||||||
|
public function onOpen(ConnectionInterface $conn) { |
||||||
|
$this->clients->attach($conn); |
||||||
|
} |
||||||
|
|
||||||
|
public function onMessage(ConnectionInterface $from, $msg) { |
||||||
|
foreach ($this->clients as $client) { |
||||||
|
if ($from != $client) { |
||||||
|
$client->send($msg); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public function onClose(ConnectionInterface $conn) { |
||||||
|
$this->clients->detach($conn); |
||||||
|
} |
||||||
|
|
||||||
|
public function onError(ConnectionInterface $conn, \Exception $e) { |
||||||
|
$conn->close(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Run the server application through the WebSocket protocol on port 8080 |
||||||
|
$app = new Ratchet\App('localhost', 8080); |
||||||
|
$app->route('/chat', new MyChat); |
||||||
|
$app->route('/echo', new Ratchet\Server\EchoServer, array('*')); |
||||||
|
$app->run(); |
||||||
|
``` |
||||||
|
|
||||||
|
$ php chat.php |
||||||
|
|
||||||
|
```javascript |
||||||
|
// Then some JavaScript in the browser: |
||||||
|
var conn = new WebSocket('ws://localhost:8080/echo'); |
||||||
|
conn.onmessage = function(e) { console.log(e.data); }; |
||||||
|
conn.send('Hello Me!'); |
||||||
|
``` |
@ -0,0 +1,32 @@ |
|||||||
|
{ |
||||||
|
"name": "cboden/ratchet" |
||||||
|
, "type": "library" |
||||||
|
, "description": "PHP WebSocket library" |
||||||
|
, "keywords": ["WebSockets", "Server", "Ratchet", "Sockets"] |
||||||
|
, "homepage": "http://socketo.me" |
||||||
|
, "license": "MIT" |
||||||
|
, "authors": [ |
||||||
|
{ |
||||||
|
"name": "Chris Boden" |
||||||
|
, "email": "cboden@gmail.com" |
||||||
|
, "role": "Developer" |
||||||
|
} |
||||||
|
] |
||||||
|
, "support": { |
||||||
|
"forum": "https://groups.google.com/forum/#!forum/ratchet-php" |
||||||
|
, "issues": "https://github.com/ratchetphp/Ratchet/issues" |
||||||
|
, "irc": "irc://irc.freenode.org/reactphp" |
||||||
|
} |
||||||
|
, "autoload": { |
||||||
|
"psr-4": { |
||||||
|
"Ratchet\\": "src/Ratchet" |
||||||
|
} |
||||||
|
} |
||||||
|
, "require": { |
||||||
|
"php": ">=5.3.9" |
||||||
|
, "react/socket": "^0.3 || ^0.4" |
||||||
|
, "guzzle/http": "^3.6" |
||||||
|
, "symfony/http-foundation": "^2.2|^3.0" |
||||||
|
, "symfony/routing": "^2.2|^3.0" |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||||
|
<phpunit |
||||||
|
forceCoversAnnotation="true" |
||||||
|
mapTestClassNameToCoveredClassName="true" |
||||||
|
bootstrap="tests/bootstrap.php" |
||||||
|
colors="true" |
||||||
|
backupGlobals="false" |
||||||
|
backupStaticAttributes="false" |
||||||
|
syntaxCheck="false" |
||||||
|
stopOnError="false" |
||||||
|
> |
||||||
|
|
||||||
|
<testsuites> |
||||||
|
<testsuite name="unit"> |
||||||
|
<directory>./tests/unit/</directory> |
||||||
|
</testsuite> |
||||||
|
</testsuites> |
||||||
|
|
||||||
|
<testsuites> |
||||||
|
<testsuite name="integration"> |
||||||
|
<directory>./tests/integration/</directory> |
||||||
|
</testsuite> |
||||||
|
</testsuites> |
||||||
|
|
||||||
|
<filter> |
||||||
|
<whitelist> |
||||||
|
<directory>./src/</directory> |
||||||
|
</whitelist> |
||||||
|
</filter> |
||||||
|
</phpunit> |
@ -0,0 +1,41 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet; |
||||||
|
|
||||||
|
/** |
||||||
|
* Wraps ConnectionInterface objects via the decorator pattern but allows |
||||||
|
* parameters to bubble through with magic methods |
||||||
|
* @todo It sure would be nice if I could make most of this a trait... |
||||||
|
*/ |
||||||
|
abstract class AbstractConnectionDecorator implements ConnectionInterface { |
||||||
|
/** |
||||||
|
* @var ConnectionInterface |
||||||
|
*/ |
||||||
|
protected $wrappedConn; |
||||||
|
|
||||||
|
public function __construct(ConnectionInterface $conn) { |
||||||
|
$this->wrappedConn = $conn; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @return ConnectionInterface |
||||||
|
*/ |
||||||
|
protected function getConnection() { |
||||||
|
return $this->wrappedConn; |
||||||
|
} |
||||||
|
|
||||||
|
public function __set($name, $value) { |
||||||
|
$this->wrappedConn->$name = $value; |
||||||
|
} |
||||||
|
|
||||||
|
public function __get($name) { |
||||||
|
return $this->wrappedConn->$name; |
||||||
|
} |
||||||
|
|
||||||
|
public function __isset($name) { |
||||||
|
return isset($this->wrappedConn->$name); |
||||||
|
} |
||||||
|
|
||||||
|
public function __unset($name) { |
||||||
|
unset($this->wrappedConn->$name); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,146 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet; |
||||||
|
use React\EventLoop\LoopInterface; |
||||||
|
use React\EventLoop\Factory as LoopFactory; |
||||||
|
use React\Socket\Server as Reactor; |
||||||
|
use Ratchet\Http\HttpServerInterface; |
||||||
|
use Ratchet\Http\OriginCheck; |
||||||
|
use Ratchet\Wamp\WampServerInterface; |
||||||
|
use Ratchet\Server\IoServer; |
||||||
|
use Ratchet\Server\FlashPolicy; |
||||||
|
use Ratchet\Http\HttpServer; |
||||||
|
use Ratchet\Http\Router; |
||||||
|
use Ratchet\WebSocket\WsServer; |
||||||
|
use Ratchet\Wamp\WampServer; |
||||||
|
use Symfony\Component\Routing\RouteCollection; |
||||||
|
use Symfony\Component\Routing\Route; |
||||||
|
use Symfony\Component\Routing\RequestContext; |
||||||
|
use Symfony\Component\Routing\Matcher\UrlMatcher; |
||||||
|
|
||||||
|
/** |
||||||
|
* An opinionated facade class to quickly and easily create a WebSocket server. |
||||||
|
* A few configuration assumptions are made and some best-practice security conventions are applied by default. |
||||||
|
*/ |
||||||
|
class App { |
||||||
|
/** |
||||||
|
* @var \Symfony\Component\Routing\RouteCollection |
||||||
|
*/ |
||||||
|
public $routes; |
||||||
|
|
||||||
|
/** |
||||||
|
* @var \Ratchet\Server\IoServer |
||||||
|
*/ |
||||||
|
public $flashServer; |
||||||
|
|
||||||
|
/** |
||||||
|
* @var \Ratchet\Server\IoServer |
||||||
|
*/ |
||||||
|
protected $_server; |
||||||
|
|
||||||
|
/** |
||||||
|
* The Host passed in construct used for same origin policy |
||||||
|
* @var string |
||||||
|
*/ |
||||||
|
protected $httpHost; |
||||||
|
|
||||||
|
/*** |
||||||
|
* The port the socket is listening |
||||||
|
* @var int |
||||||
|
*/ |
||||||
|
protected $port; |
||||||
|
|
||||||
|
/** |
||||||
|
* @var int |
||||||
|
*/ |
||||||
|
protected $_routeCounter = 0; |
||||||
|
|
||||||
|
/** |
||||||
|
* @param string $httpHost HTTP hostname clients intend to connect to. MUST match JS `new WebSocket('ws://$httpHost');` |
||||||
|
* @param int $port Port to listen on. If 80, assuming production, Flash on 843 otherwise expecting Flash to be proxied through 8843 |
||||||
|
* @param string $address IP address to bind to. Default is localhost/proxy only. '0.0.0.0' for any machine. |
||||||
|
* @param LoopInterface $loop Specific React\EventLoop to bind the application to. null will create one for you. |
||||||
|
*/ |
||||||
|
public function __construct($httpHost = 'localhost', $port = 8080, $address = '127.0.0.1', LoopInterface $loop = null) { |
||||||
|
if (extension_loaded('xdebug')) { |
||||||
|
trigger_error('XDebug extension detected. Remember to disable this if performance testing or going live!', E_USER_WARNING); |
||||||
|
} |
||||||
|
|
||||||
|
if (3 !== strlen('✓')) { |
||||||
|
throw new \DomainException('Bad encoding, length of unicode character ✓ should be 3. Ensure charset UTF-8 and check ini val mbstring.func_autoload'); |
||||||
|
} |
||||||
|
|
||||||
|
if (null === $loop) { |
||||||
|
$loop = LoopFactory::create(); |
||||||
|
} |
||||||
|
|
||||||
|
$this->httpHost = $httpHost; |
||||||
|
$this->port = $port; |
||||||
|
|
||||||
|
$socket = new Reactor($loop); |
||||||
|
$socket->listen($port, $address); |
||||||
|
|
||||||
|
$this->routes = new RouteCollection; |
||||||
|
$this->_server = new IoServer(new HttpServer(new Router(new UrlMatcher($this->routes, new RequestContext))), $socket, $loop); |
||||||
|
|
||||||
|
$policy = new FlashPolicy; |
||||||
|
$policy->addAllowedAccess($httpHost, 80); |
||||||
|
$policy->addAllowedAccess($httpHost, $port); |
||||||
|
$flashSock = new Reactor($loop); |
||||||
|
$this->flashServer = new IoServer($policy, $flashSock); |
||||||
|
if (80 == $port) { |
||||||
|
$flashSock->listen(843, '0.0.0.0'); |
||||||
|
} else { |
||||||
|
$flashSock->listen(8843); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Add an endpoint/application to the server |
||||||
|
* @param string $path The URI the client will connect to |
||||||
|
* @param ComponentInterface $controller Your application to server for the route. If not specified, assumed to be for a WebSocket |
||||||
|
* @param array $allowedOrigins An array of hosts allowed to connect (same host by default), ['*'] for any |
||||||
|
* @param string $httpHost Override the $httpHost variable provided in the __construct |
||||||
|
* @return ComponentInterface|WsServer |
||||||
|
*/ |
||||||
|
public function route($path, ComponentInterface $controller, array $allowedOrigins = array(), $httpHost = null) { |
||||||
|
if ($controller instanceof HttpServerInterface || $controller instanceof WsServer) { |
||||||
|
$decorated = $controller; |
||||||
|
} elseif ($controller instanceof WampServerInterface) { |
||||||
|
$decorated = new WsServer(new WampServer($controller)); |
||||||
|
} elseif ($controller instanceof MessageComponentInterface) { |
||||||
|
$decorated = new WsServer($controller); |
||||||
|
} else { |
||||||
|
$decorated = $controller; |
||||||
|
} |
||||||
|
|
||||||
|
if ($httpHost === null) { |
||||||
|
$httpHost = $this->httpHost; |
||||||
|
} |
||||||
|
|
||||||
|
$allowedOrigins = array_values($allowedOrigins); |
||||||
|
if (0 === count($allowedOrigins)) { |
||||||
|
$allowedOrigins[] = $httpHost; |
||||||
|
} |
||||||
|
if ('*' !== $allowedOrigins[0]) { |
||||||
|
$decorated = new OriginCheck($decorated, $allowedOrigins); |
||||||
|
} |
||||||
|
|
||||||
|
//allow origins in flash policy server |
||||||
|
if(empty($this->flashServer) === false) { |
||||||
|
foreach($allowedOrigins as $allowedOrgin) { |
||||||
|
$this->flashServer->app->addAllowedAccess($allowedOrgin, $this->port); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
$this->routes->add('rr-' . ++$this->_routeCounter, new Route($path, array('_controller' => $decorated), array('Origin' => $this->httpHost), array(), $httpHost)); |
||||||
|
|
||||||
|
return $decorated; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Run the server by entering the event loop |
||||||
|
*/ |
||||||
|
public function run() { |
||||||
|
$this->_server->run(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,31 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet; |
||||||
|
|
||||||
|
/** |
||||||
|
* This is the interface to build a Ratchet application with. |
||||||
|
* It implements the decorator pattern to build an application stack |
||||||
|
*/ |
||||||
|
interface ComponentInterface { |
||||||
|
/** |
||||||
|
* When a new connection is opened it will be passed to this method |
||||||
|
* @param ConnectionInterface $conn The socket/connection that just connected to your application |
||||||
|
* @throws \Exception |
||||||
|
*/ |
||||||
|
function onOpen(ConnectionInterface $conn); |
||||||
|
|
||||||
|
/** |
||||||
|
* This is called before or after a socket is closed (depends on how it's closed). SendMessage to $conn will not result in an error if it has already been closed. |
||||||
|
* @param ConnectionInterface $conn The socket/connection that is closing/closed |
||||||
|
* @throws \Exception |
||||||
|
*/ |
||||||
|
function onClose(ConnectionInterface $conn); |
||||||
|
|
||||||
|
/** |
||||||
|
* If there is an error with one of the sockets, or somewhere in the application where an Exception is thrown, |
||||||
|
* the Exception is sent back down the stack, handled by the Server and bubbled back up the application through this method |
||||||
|
* @param ConnectionInterface $conn |
||||||
|
* @param \Exception $e |
||||||
|
* @throws \Exception |
||||||
|
*/ |
||||||
|
function onError(ConnectionInterface $conn, \Exception $e); |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet; |
||||||
|
|
||||||
|
/** |
||||||
|
* The version of Ratchet being used |
||||||
|
* @var string |
||||||
|
*/ |
||||||
|
const VERSION = 'Ratchet/0.3.5'; |
||||||
|
|
||||||
|
/** |
||||||
|
* A proxy object representing a connection to the application |
||||||
|
* This acts as a container to store data (in memory) about the connection |
||||||
|
*/ |
||||||
|
interface ConnectionInterface { |
||||||
|
/** |
||||||
|
* Send data to the connection |
||||||
|
* @param string $data |
||||||
|
* @return \Ratchet\ConnectionInterface |
||||||
|
*/ |
||||||
|
function send($data); |
||||||
|
|
||||||
|
/** |
||||||
|
* Close the connection |
||||||
|
*/ |
||||||
|
function close(); |
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Http\Guzzle\Http\Message; |
||||||
|
use Guzzle\Http\Message\RequestFactory as GuzzleRequestFactory; |
||||||
|
use Guzzle\Http\EntityBody; |
||||||
|
|
||||||
|
class RequestFactory extends GuzzleRequestFactory { |
||||||
|
|
||||||
|
protected static $ratchetInstance; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public static function getInstance() |
||||||
|
{ |
||||||
|
// @codeCoverageIgnoreStart |
||||||
|
if (!static::$ratchetInstance) { |
||||||
|
static::$ratchetInstance = new static(); |
||||||
|
} |
||||||
|
// @codeCoverageIgnoreEnd |
||||||
|
|
||||||
|
return static::$ratchetInstance; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function create($method, $url, $headers = null, $body = '', array $options = array()) { |
||||||
|
$c = $this->entityEnclosingRequestClass; |
||||||
|
$request = new $c($method, $url, $headers); |
||||||
|
$request->setBody(EntityBody::factory($body)); |
||||||
|
|
||||||
|
return $request; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,56 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Http; |
||||||
|
use Ratchet\MessageInterface; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
use Ratchet\Http\Guzzle\Http\Message\RequestFactory; |
||||||
|
|
||||||
|
/** |
||||||
|
* This class receives streaming data from a client request |
||||||
|
* and parses HTTP headers, returning a Guzzle Request object |
||||||
|
* once it's been buffered |
||||||
|
*/ |
||||||
|
class HttpRequestParser implements MessageInterface { |
||||||
|
const EOM = "\r\n\r\n"; |
||||||
|
|
||||||
|
/** |
||||||
|
* The maximum number of bytes the request can be |
||||||
|
* This is a security measure to prevent attacks |
||||||
|
* @var int |
||||||
|
*/ |
||||||
|
public $maxSize = 4096; |
||||||
|
|
||||||
|
/** |
||||||
|
* @param \Ratchet\ConnectionInterface $context |
||||||
|
* @param string $data Data stream to buffer |
||||||
|
* @return \Guzzle\Http\Message\RequestInterface|null |
||||||
|
* @throws \OverflowException If the message buffer has become too large |
||||||
|
*/ |
||||||
|
public function onMessage(ConnectionInterface $context, $data) { |
||||||
|
if (!isset($context->httpBuffer)) { |
||||||
|
$context->httpBuffer = ''; |
||||||
|
} |
||||||
|
|
||||||
|
$context->httpBuffer .= $data; |
||||||
|
|
||||||
|
if (strlen($context->httpBuffer) > (int)$this->maxSize) { |
||||||
|
throw new \OverflowException("Maximum buffer size of {$this->maxSize} exceeded parsing HTTP header"); |
||||||
|
} |
||||||
|
|
||||||
|
if ($this->isEom($context->httpBuffer)) { |
||||||
|
$request = RequestFactory::getInstance()->fromMessage($context->httpBuffer); |
||||||
|
|
||||||
|
unset($context->httpBuffer); |
||||||
|
|
||||||
|
return $request; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Determine if the message has been buffered as per the HTTP specification |
||||||
|
* @param string $message |
||||||
|
* @return boolean |
||||||
|
*/ |
||||||
|
public function isEom($message) { |
||||||
|
return (boolean)strpos($message, static::EOM); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,90 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Http; |
||||||
|
use Ratchet\MessageComponentInterface; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
use Guzzle\Http\Message\Response; |
||||||
|
|
||||||
|
class HttpServer implements MessageComponentInterface { |
||||||
|
/** |
||||||
|
* Buffers incoming HTTP requests returning a Guzzle Request when coalesced |
||||||
|
* @var HttpRequestParser |
||||||
|
* @note May not expose this in the future, may do through facade methods |
||||||
|
*/ |
||||||
|
protected $_reqParser; |
||||||
|
|
||||||
|
/** |
||||||
|
* @var \Ratchet\Http\HttpServerInterface |
||||||
|
*/ |
||||||
|
protected $_httpServer; |
||||||
|
|
||||||
|
/** |
||||||
|
* @param HttpServerInterface |
||||||
|
*/ |
||||||
|
public function __construct(HttpServerInterface $component) { |
||||||
|
$this->_httpServer = $component; |
||||||
|
$this->_reqParser = new HttpRequestParser; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onOpen(ConnectionInterface $conn) { |
||||||
|
$conn->httpHeadersReceived = false; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onMessage(ConnectionInterface $from, $msg) { |
||||||
|
if (true !== $from->httpHeadersReceived) { |
||||||
|
try { |
||||||
|
if (null === ($request = $this->_reqParser->onMessage($from, $msg))) { |
||||||
|
return; |
||||||
|
} |
||||||
|
} catch (\OverflowException $oe) { |
||||||
|
return $this->close($from, 413); |
||||||
|
} |
||||||
|
|
||||||
|
$from->httpHeadersReceived = true; |
||||||
|
|
||||||
|
return $this->_httpServer->onOpen($from, $request); |
||||||
|
} |
||||||
|
|
||||||
|
$this->_httpServer->onMessage($from, $msg); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onClose(ConnectionInterface $conn) { |
||||||
|
if ($conn->httpHeadersReceived) { |
||||||
|
$this->_httpServer->onClose($conn); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onError(ConnectionInterface $conn, \Exception $e) { |
||||||
|
if ($conn->httpHeadersReceived) { |
||||||
|
$this->_httpServer->onError($conn, $e); |
||||||
|
} else { |
||||||
|
$this->close($conn, 500); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Close a connection with an HTTP response |
||||||
|
* @param \Ratchet\ConnectionInterface $conn |
||||||
|
* @param int $code HTTP status code |
||||||
|
* @return null |
||||||
|
*/ |
||||||
|
protected function close(ConnectionInterface $conn, $code = 400) { |
||||||
|
$response = new Response($code, array( |
||||||
|
'X-Powered-By' => \Ratchet\VERSION |
||||||
|
)); |
||||||
|
|
||||||
|
$conn->send((string)$response); |
||||||
|
$conn->close(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Http; |
||||||
|
use Ratchet\MessageComponentInterface; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
use Guzzle\Http\Message\RequestInterface; |
||||||
|
|
||||||
|
interface HttpServerInterface extends MessageComponentInterface { |
||||||
|
/** |
||||||
|
* @param \Ratchet\ConnectionInterface $conn |
||||||
|
* @param \Guzzle\Http\Message\RequestInterface $request null is default because PHP won't let me overload; don't pass null!!! |
||||||
|
* @throws \UnexpectedValueException if a RequestInterface is not passed |
||||||
|
*/ |
||||||
|
public function onOpen(ConnectionInterface $conn, RequestInterface $request = null); |
||||||
|
} |
@ -0,0 +1,79 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Http; |
||||||
|
use Guzzle\Http\Message\RequestInterface; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
use Ratchet\MessageComponentInterface; |
||||||
|
use Guzzle\Http\Message\Response; |
||||||
|
|
||||||
|
/** |
||||||
|
* A middleware to ensure JavaScript clients connecting are from the expected domain. |
||||||
|
* This protects other websites from open WebSocket connections to your application. |
||||||
|
* Note: This can be spoofed from non-web browser clients |
||||||
|
*/ |
||||||
|
class OriginCheck implements HttpServerInterface { |
||||||
|
/** |
||||||
|
* @var \Ratchet\MessageComponentInterface |
||||||
|
*/ |
||||||
|
protected $_component; |
||||||
|
|
||||||
|
public $allowedOrigins = array(); |
||||||
|
|
||||||
|
/** |
||||||
|
* @param MessageComponentInterface $component Component/Application to decorate |
||||||
|
* @param array $allowed An array of allowed domains that are allowed to connect from |
||||||
|
*/ |
||||||
|
public function __construct(MessageComponentInterface $component, array $allowed = array()) { |
||||||
|
$this->_component = $component; |
||||||
|
$this->allowedOrigins += $allowed; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) { |
||||||
|
$header = (string)$request->getHeader('Origin'); |
||||||
|
$origin = parse_url($header, PHP_URL_HOST) ?: $header; |
||||||
|
|
||||||
|
if (!in_array($origin, $this->allowedOrigins)) { |
||||||
|
return $this->close($conn, 403); |
||||||
|
} |
||||||
|
|
||||||
|
return $this->_component->onOpen($conn, $request); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
function onMessage(ConnectionInterface $from, $msg) { |
||||||
|
return $this->_component->onMessage($from, $msg); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
function onClose(ConnectionInterface $conn) { |
||||||
|
return $this->_component->onClose($conn); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
function onError(ConnectionInterface $conn, \Exception $e) { |
||||||
|
return $this->_component->onError($conn, $e); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Close a connection with an HTTP response |
||||||
|
* @param \Ratchet\ConnectionInterface $conn |
||||||
|
* @param int $code HTTP status code |
||||||
|
* @return null |
||||||
|
*/ |
||||||
|
protected function close(ConnectionInterface $conn, $code = 400) { |
||||||
|
$response = new Response($code, array( |
||||||
|
'X-Powered-By' => \Ratchet\VERSION |
||||||
|
)); |
||||||
|
|
||||||
|
$conn->send((string)$response); |
||||||
|
$conn->close(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,105 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Http; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
use Guzzle\Http\Message\RequestInterface; |
||||||
|
use Guzzle\Http\Message\Response; |
||||||
|
use Guzzle\Http\Url; |
||||||
|
use Symfony\Component\Routing\Matcher\UrlMatcherInterface; |
||||||
|
use Symfony\Component\Routing\Exception\MethodNotAllowedException; |
||||||
|
use Symfony\Component\Routing\Exception\ResourceNotFoundException; |
||||||
|
|
||||||
|
class Router implements HttpServerInterface { |
||||||
|
/** |
||||||
|
* @var \Symfony\Component\Routing\Matcher\UrlMatcherInterface |
||||||
|
*/ |
||||||
|
protected $_matcher; |
||||||
|
|
||||||
|
public function __construct(UrlMatcherInterface $matcher) { |
||||||
|
$this->_matcher = $matcher; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
* @throws \UnexpectedValueException If a controller is not \Ratchet\Http\HttpServerInterface |
||||||
|
*/ |
||||||
|
public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) { |
||||||
|
if (null === $request) { |
||||||
|
throw new \UnexpectedValueException('$request can not be null'); |
||||||
|
} |
||||||
|
|
||||||
|
$context = $this->_matcher->getContext(); |
||||||
|
$context->setMethod($request->getMethod()); |
||||||
|
$context->setHost($request->getHost()); |
||||||
|
|
||||||
|
try { |
||||||
|
$route = $this->_matcher->match($request->getPath()); |
||||||
|
} catch (MethodNotAllowedException $nae) { |
||||||
|
return $this->close($conn, 403); |
||||||
|
} catch (ResourceNotFoundException $nfe) { |
||||||
|
return $this->close($conn, 404); |
||||||
|
} |
||||||
|
|
||||||
|
if (is_string($route['_controller']) && class_exists($route['_controller'])) { |
||||||
|
$route['_controller'] = new $route['_controller']; |
||||||
|
} |
||||||
|
|
||||||
|
if (!($route['_controller'] instanceof HttpServerInterface)) { |
||||||
|
throw new \UnexpectedValueException('All routes must implement Ratchet\Http\HttpServerInterface'); |
||||||
|
} |
||||||
|
|
||||||
|
$parameters = array(); |
||||||
|
foreach($route as $key => $value) { |
||||||
|
if ((is_string($key)) && ('_' !== substr($key, 0, 1))) { |
||||||
|
$parameters[$key] = $value; |
||||||
|
} |
||||||
|
} |
||||||
|
$parameters = array_merge($parameters, $request->getQuery()->getAll()); |
||||||
|
|
||||||
|
$url = Url::factory($request->getPath()); |
||||||
|
$url->setQuery($parameters); |
||||||
|
$request->setUrl($url); |
||||||
|
|
||||||
|
$conn->controller = $route['_controller']; |
||||||
|
$conn->controller->onOpen($conn, $request); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
function onMessage(ConnectionInterface $from, $msg) { |
||||||
|
$from->controller->onMessage($from, $msg); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
function onClose(ConnectionInterface $conn) { |
||||||
|
if (isset($conn->controller)) { |
||||||
|
$conn->controller->onClose($conn); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
function onError(ConnectionInterface $conn, \Exception $e) { |
||||||
|
if (isset($conn->controller)) { |
||||||
|
$conn->controller->onError($conn, $e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Close a connection with an HTTP response |
||||||
|
* @param \Ratchet\ConnectionInterface $conn |
||||||
|
* @param int $code HTTP status code |
||||||
|
* @return null |
||||||
|
*/ |
||||||
|
protected function close(ConnectionInterface $conn, $code = 400) { |
||||||
|
$response = new Response($code, array( |
||||||
|
'X-Powered-By' => \Ratchet\VERSION |
||||||
|
)); |
||||||
|
|
||||||
|
$conn->send((string)$response); |
||||||
|
$conn->close(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet; |
||||||
|
|
||||||
|
interface MessageComponentInterface extends ComponentInterface, MessageInterface { |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet; |
||||||
|
|
||||||
|
interface MessageInterface { |
||||||
|
/** |
||||||
|
* Triggered when a client sends data through the socket |
||||||
|
* @param \Ratchet\ConnectionInterface $from The socket/connection that sent the message to your application |
||||||
|
* @param string $msg The message received |
||||||
|
* @throws \Exception |
||||||
|
*/ |
||||||
|
function onMessage(ConnectionInterface $from, $msg); |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Server; |
||||||
|
use Ratchet\MessageComponentInterface; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* A simple Ratchet application that will reply to all messages with the message it received |
||||||
|
*/ |
||||||
|
class EchoServer implements MessageComponentInterface { |
||||||
|
public function onOpen(ConnectionInterface $conn) { |
||||||
|
} |
||||||
|
|
||||||
|
public function onMessage(ConnectionInterface $from, $msg) { |
||||||
|
$from->send($msg); |
||||||
|
} |
||||||
|
|
||||||
|
public function onClose(ConnectionInterface $conn) { |
||||||
|
} |
||||||
|
|
||||||
|
public function onError(ConnectionInterface $conn, \Exception $e) { |
||||||
|
$conn->close(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,200 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Server; |
||||||
|
use Ratchet\MessageComponentInterface; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* An app to go on a server stack to pass a policy file to a Flash socket |
||||||
|
* Useful if you're using Flash as a WebSocket polyfill on IE |
||||||
|
* Be sure to run your server instance on port 843 |
||||||
|
* By default this lets accepts everything, make sure you tighten the rules up for production |
||||||
|
* @final |
||||||
|
* @link http://www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html |
||||||
|
* @link http://learn.adobe.com/wiki/download/attachments/64389123/CrossDomain_PolicyFile_Specification.pdf?version=1 |
||||||
|
* @link view-source:http://www.adobe.com/xml/schemas/PolicyFileSocket.xsd |
||||||
|
*/ |
||||||
|
class FlashPolicy implements MessageComponentInterface { |
||||||
|
|
||||||
|
/** |
||||||
|
* Contains the root policy node |
||||||
|
* @var string |
||||||
|
*/ |
||||||
|
protected $_policy = '<?xml version="1.0"?><!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd"><cross-domain-policy></cross-domain-policy>'; |
||||||
|
|
||||||
|
/** |
||||||
|
* Stores an array of allowed domains and their ports |
||||||
|
* @var array |
||||||
|
*/ |
||||||
|
protected $_access = array(); |
||||||
|
|
||||||
|
/** |
||||||
|
* @var string |
||||||
|
*/ |
||||||
|
protected $_siteControl = ''; |
||||||
|
|
||||||
|
/** |
||||||
|
* @var string |
||||||
|
*/ |
||||||
|
protected $_cache = ''; |
||||||
|
|
||||||
|
/** |
||||||
|
* @var string |
||||||
|
*/ |
||||||
|
protected $_cacheValid = false; |
||||||
|
|
||||||
|
/** |
||||||
|
* Add a domain to an allowed access list. |
||||||
|
* |
||||||
|
* @param string $domain Specifies a requesting domain to be granted access. Both named domains and IP |
||||||
|
* addresses are acceptable values. Subdomains are considered different domains. A wildcard (*) can |
||||||
|
* be used to match all domains when used alone, or multiple domains (subdomains) when used as a |
||||||
|
* prefix for an explicit, second-level domain name separated with a dot (.) |
||||||
|
* @param string $ports A comma-separated list of ports or range of ports that a socket connection |
||||||
|
* is allowed to connect to. A range of ports is specified through a dash (-) between two port numbers. |
||||||
|
* Ranges can be used with individual ports when separated with a comma. A single wildcard (*) can |
||||||
|
* be used to allow all ports. |
||||||
|
* @param bool $secure |
||||||
|
* @throws \UnexpectedValueException |
||||||
|
* @return FlashPolicy |
||||||
|
*/ |
||||||
|
public function addAllowedAccess($domain, $ports = '*', $secure = false) { |
||||||
|
if (!$this->validateDomain($domain)) { |
||||||
|
throw new \UnexpectedValueException('Invalid domain'); |
||||||
|
} |
||||||
|
|
||||||
|
if (!$this->validatePorts($ports)) { |
||||||
|
throw new \UnexpectedValueException('Invalid Port'); |
||||||
|
} |
||||||
|
|
||||||
|
$this->_access[] = array($domain, $ports, (boolean)$secure); |
||||||
|
$this->_cacheValid = false; |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Removes all domains from the allowed access list. |
||||||
|
* |
||||||
|
* @return \Ratchet\Server\FlashPolicy |
||||||
|
*/ |
||||||
|
public function clearAllowedAccess() { |
||||||
|
$this->_access = array(); |
||||||
|
$this->_cacheValid = false; |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* site-control defines the meta-policy for the current domain. A meta-policy specifies acceptable |
||||||
|
* domain policy files other than the master policy file located in the target domain's root and named |
||||||
|
* crossdomain.xml. |
||||||
|
* |
||||||
|
* @param string $permittedCrossDomainPolicies |
||||||
|
* @throws \UnexpectedValueException |
||||||
|
* @return FlashPolicy |
||||||
|
*/ |
||||||
|
public function setSiteControl($permittedCrossDomainPolicies = 'all') { |
||||||
|
if (!$this->validateSiteControl($permittedCrossDomainPolicies)) { |
||||||
|
throw new \UnexpectedValueException('Invalid site control set'); |
||||||
|
} |
||||||
|
|
||||||
|
$this->_siteControl = $permittedCrossDomainPolicies; |
||||||
|
$this->_cacheValid = false; |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onOpen(ConnectionInterface $conn) { |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onMessage(ConnectionInterface $from, $msg) { |
||||||
|
if (!$this->_cacheValid) { |
||||||
|
$this->_cache = $this->renderPolicy()->asXML(); |
||||||
|
$this->_cacheValid = true; |
||||||
|
} |
||||||
|
|
||||||
|
$from->send($this->_cache . "\0"); |
||||||
|
$from->close(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onClose(ConnectionInterface $conn) { |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onError(ConnectionInterface $conn, \Exception $e) { |
||||||
|
$conn->close(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Builds the crossdomain file based on the template policy |
||||||
|
* |
||||||
|
* @throws \UnexpectedValueException |
||||||
|
* @return \SimpleXMLElement |
||||||
|
*/ |
||||||
|
public function renderPolicy() { |
||||||
|
$policy = new \SimpleXMLElement($this->_policy); |
||||||
|
|
||||||
|
$siteControl = $policy->addChild('site-control'); |
||||||
|
|
||||||
|
if ($this->_siteControl == '') { |
||||||
|
$this->setSiteControl(); |
||||||
|
} |
||||||
|
|
||||||
|
$siteControl->addAttribute('permitted-cross-domain-policies', $this->_siteControl); |
||||||
|
|
||||||
|
if (empty($this->_access)) { |
||||||
|
throw new \UnexpectedValueException('You must add a domain through addAllowedAccess()'); |
||||||
|
} |
||||||
|
|
||||||
|
foreach ($this->_access as $access) { |
||||||
|
$tmp = $policy->addChild('allow-access-from'); |
||||||
|
$tmp->addAttribute('domain', $access[0]); |
||||||
|
$tmp->addAttribute('to-ports', $access[1]); |
||||||
|
$tmp->addAttribute('secure', ($access[2] === true) ? 'true' : 'false'); |
||||||
|
} |
||||||
|
|
||||||
|
return $policy; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Make sure the proper site control was passed |
||||||
|
* |
||||||
|
* @param string $permittedCrossDomainPolicies |
||||||
|
* @return bool |
||||||
|
*/ |
||||||
|
public function validateSiteControl($permittedCrossDomainPolicies) { |
||||||
|
//'by-content-type' and 'by-ftp-filename' are not available for sockets |
||||||
|
return (bool)in_array($permittedCrossDomainPolicies, array('none', 'master-only', 'all')); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Validate for proper domains (wildcards allowed) |
||||||
|
* |
||||||
|
* @param string $domain |
||||||
|
* @return bool |
||||||
|
*/ |
||||||
|
public function validateDomain($domain) { |
||||||
|
return (bool)preg_match("/^((http(s)?:\/\/)?([a-z0-9-_]+\.|\*\.)*([a-z0-9-_\.]+)|\*)$/i", $domain); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Make sure valid ports were passed |
||||||
|
* |
||||||
|
* @param string $port |
||||||
|
* @return bool |
||||||
|
*/ |
||||||
|
public function validatePorts($port) { |
||||||
|
return (bool)preg_match('/^(\*|(\d+[,-]?)*\d+)$/', $port); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,38 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Server; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
use React\Socket\ConnectionInterface as ReactConn; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
class IoConnection implements ConnectionInterface { |
||||||
|
/** |
||||||
|
* @var \React\Socket\ConnectionInterface |
||||||
|
*/ |
||||||
|
protected $conn; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* @param \React\Socket\ConnectionInterface $conn |
||||||
|
*/ |
||||||
|
public function __construct(ReactConn $conn) { |
||||||
|
$this->conn = $conn; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function send($data) { |
||||||
|
$this->conn->write($data); |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function close() { |
||||||
|
$this->conn->end(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,141 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Server; |
||||||
|
use Ratchet\MessageComponentInterface; |
||||||
|
use React\EventLoop\LoopInterface; |
||||||
|
use React\Socket\ServerInterface; |
||||||
|
use React\EventLoop\Factory as LoopFactory; |
||||||
|
use React\Socket\Server as Reactor; |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates an open-ended socket to listen on a port for incoming connections. |
||||||
|
* Events are delegated through this to attached applications |
||||||
|
*/ |
||||||
|
class IoServer { |
||||||
|
/** |
||||||
|
* @var \React\EventLoop\LoopInterface |
||||||
|
*/ |
||||||
|
public $loop; |
||||||
|
|
||||||
|
/** |
||||||
|
* @var \Ratchet\MessageComponentInterface |
||||||
|
*/ |
||||||
|
public $app; |
||||||
|
|
||||||
|
/** |
||||||
|
* Array of React event handlers |
||||||
|
* @var \SplFixedArray |
||||||
|
*/ |
||||||
|
protected $handlers; |
||||||
|
|
||||||
|
/** |
||||||
|
* The socket server the Ratchet Application is run off of |
||||||
|
* @var \React\Socket\ServerInterface |
||||||
|
*/ |
||||||
|
public $socket; |
||||||
|
|
||||||
|
/** |
||||||
|
* @param \Ratchet\MessageComponentInterface $app The Ratchet application stack to host |
||||||
|
* @param \React\Socket\ServerInterface $socket The React socket server to run the Ratchet application off of |
||||||
|
* @param \React\EventLoop\LoopInterface|null $loop The React looper to run the Ratchet application off of |
||||||
|
*/ |
||||||
|
public function __construct(MessageComponentInterface $app, ServerInterface $socket, LoopInterface $loop = null) { |
||||||
|
if (false === strpos(PHP_VERSION, "hiphop")) { |
||||||
|
gc_enable(); |
||||||
|
} |
||||||
|
|
||||||
|
set_time_limit(0); |
||||||
|
ob_implicit_flush(); |
||||||
|
|
||||||
|
$this->loop = $loop; |
||||||
|
$this->app = $app; |
||||||
|
$this->socket = $socket; |
||||||
|
|
||||||
|
$socket->on('connection', array($this, 'handleConnect')); |
||||||
|
|
||||||
|
$this->handlers = new \SplFixedArray(3); |
||||||
|
$this->handlers[0] = array($this, 'handleData'); |
||||||
|
$this->handlers[1] = array($this, 'handleEnd'); |
||||||
|
$this->handlers[2] = array($this, 'handleError'); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param \Ratchet\MessageComponentInterface $component The application that I/O will call when events are received |
||||||
|
* @param int $port The port to server sockets on |
||||||
|
* @param string $address The address to receive sockets on (0.0.0.0 means receive connections from any) |
||||||
|
* @return IoServer |
||||||
|
*/ |
||||||
|
public static function factory(MessageComponentInterface $component, $port = 80, $address = '0.0.0.0') { |
||||||
|
$loop = LoopFactory::create(); |
||||||
|
$socket = new Reactor($loop); |
||||||
|
$socket->listen($port, $address); |
||||||
|
|
||||||
|
return new static($component, $socket, $loop); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Run the application by entering the event loop |
||||||
|
* @throws \RuntimeException If a loop was not previously specified |
||||||
|
*/ |
||||||
|
public function run() { |
||||||
|
if (null === $this->loop) { |
||||||
|
throw new \RuntimeException("A React Loop was not provided during instantiation"); |
||||||
|
} |
||||||
|
|
||||||
|
// @codeCoverageIgnoreStart |
||||||
|
$this->loop->run(); |
||||||
|
// @codeCoverageIgnoreEnd |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Triggered when a new connection is received from React |
||||||
|
* @param \React\Socket\ConnectionInterface $conn |
||||||
|
*/ |
||||||
|
public function handleConnect($conn) { |
||||||
|
$conn->decor = new IoConnection($conn); |
||||||
|
|
||||||
|
$conn->decor->resourceId = (int)$conn->stream; |
||||||
|
$conn->decor->remoteAddress = $conn->getRemoteAddress(); |
||||||
|
|
||||||
|
$this->app->onOpen($conn->decor); |
||||||
|
|
||||||
|
$conn->on('data', $this->handlers[0]); |
||||||
|
$conn->on('end', $this->handlers[1]); |
||||||
|
$conn->on('error', $this->handlers[2]); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Data has been received from React |
||||||
|
* @param string $data |
||||||
|
* @param \React\Socket\ConnectionInterface $conn |
||||||
|
*/ |
||||||
|
public function handleData($data, $conn) { |
||||||
|
try { |
||||||
|
$this->app->onMessage($conn->decor, $data); |
||||||
|
} catch (\Exception $e) { |
||||||
|
$this->handleError($e, $conn); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* A connection has been closed by React |
||||||
|
* @param \React\Socket\ConnectionInterface $conn |
||||||
|
*/ |
||||||
|
public function handleEnd($conn) { |
||||||
|
try { |
||||||
|
$this->app->onClose($conn->decor); |
||||||
|
} catch (\Exception $e) { |
||||||
|
$this->handleError($e, $conn); |
||||||
|
} |
||||||
|
|
||||||
|
unset($conn->decor); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* An error has occurred, let the listening application know |
||||||
|
* @param \Exception $e |
||||||
|
* @param \React\Socket\ConnectionInterface $conn |
||||||
|
*/ |
||||||
|
public function handleError(\Exception $e, $conn) { |
||||||
|
$this->app->onError($conn->decor, $e); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,111 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Server; |
||||||
|
use Ratchet\MessageComponentInterface; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
|
||||||
|
class IpBlackList implements MessageComponentInterface { |
||||||
|
/** |
||||||
|
* @var array |
||||||
|
*/ |
||||||
|
protected $_blacklist = array(); |
||||||
|
|
||||||
|
/** |
||||||
|
* @var \Ratchet\MessageComponentInterface |
||||||
|
*/ |
||||||
|
protected $_decorating; |
||||||
|
|
||||||
|
/** |
||||||
|
* @param \Ratchet\MessageComponentInterface $component |
||||||
|
*/ |
||||||
|
public function __construct(MessageComponentInterface $component) { |
||||||
|
$this->_decorating = $component; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Add an address to the blacklist that will not be allowed to connect to your application |
||||||
|
* @param string $ip IP address to block from connecting to your application |
||||||
|
* @return IpBlackList |
||||||
|
*/ |
||||||
|
public function blockAddress($ip) { |
||||||
|
$this->_blacklist[$ip] = true; |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Unblock an address so they can access your application again |
||||||
|
* @param string $ip IP address to unblock from connecting to your application |
||||||
|
* @return IpBlackList |
||||||
|
*/ |
||||||
|
public function unblockAddress($ip) { |
||||||
|
if (isset($this->_blacklist[$this->filterAddress($ip)])) { |
||||||
|
unset($this->_blacklist[$this->filterAddress($ip)]); |
||||||
|
} |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param string $address |
||||||
|
* @return bool |
||||||
|
*/ |
||||||
|
public function isBlocked($address) { |
||||||
|
return (isset($this->_blacklist[$this->filterAddress($address)])); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get an array of all the addresses blocked |
||||||
|
* @return array |
||||||
|
*/ |
||||||
|
public function getBlockedAddresses() { |
||||||
|
return array_keys($this->_blacklist); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param string $address |
||||||
|
* @return string |
||||||
|
*/ |
||||||
|
public function filterAddress($address) { |
||||||
|
if (strstr($address, ':') && substr_count($address, '.') == 3) { |
||||||
|
list($address, $port) = explode(':', $address); |
||||||
|
} |
||||||
|
|
||||||
|
return $address; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
function onOpen(ConnectionInterface $conn) { |
||||||
|
if ($this->isBlocked($conn->remoteAddress)) { |
||||||
|
return $conn->close(); |
||||||
|
} |
||||||
|
|
||||||
|
return $this->_decorating->onOpen($conn); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
function onMessage(ConnectionInterface $from, $msg) { |
||||||
|
return $this->_decorating->onMessage($from, $msg); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
function onClose(ConnectionInterface $conn) { |
||||||
|
if (!$this->isBlocked($conn->remoteAddress)) { |
||||||
|
$this->_decorating->onClose($conn); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
function onError(ConnectionInterface $conn, \Exception $e) { |
||||||
|
if (!$this->isBlocked($conn->remoteAddress)) { |
||||||
|
$this->_decorating->onError($conn, $e); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Session\Serialize; |
||||||
|
|
||||||
|
interface HandlerInterface { |
||||||
|
/** |
||||||
|
* @param array |
||||||
|
* @return string |
||||||
|
*/ |
||||||
|
function serialize(array $data); |
||||||
|
|
||||||
|
/** |
||||||
|
* @param string |
||||||
|
* @return array |
||||||
|
*/ |
||||||
|
function unserialize($raw); |
||||||
|
} |
@ -0,0 +1,33 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Session\Serialize; |
||||||
|
|
||||||
|
class PhpBinaryHandler implements HandlerInterface { |
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
function serialize(array $data) { |
||||||
|
throw new \RuntimeException("Serialize PhpHandler:serialize code not written yet, write me!"); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
* @link http://ca2.php.net/manual/en/function.session-decode.php#108037 Code from this comment on php.net |
||||||
|
*/ |
||||||
|
public function unserialize($raw) { |
||||||
|
$returnData = array(); |
||||||
|
$offset = 0; |
||||||
|
|
||||||
|
while ($offset < strlen($raw)) { |
||||||
|
$num = ord($raw[$offset]); |
||||||
|
$offset += 1; |
||||||
|
$varname = substr($raw, $offset, $num); |
||||||
|
$offset += $num; |
||||||
|
$data = unserialize(substr($raw, $offset)); |
||||||
|
|
||||||
|
$returnData[$varname] = $data; |
||||||
|
$offset += strlen(serialize($data)); |
||||||
|
} |
||||||
|
|
||||||
|
return $returnData; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,49 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Session\Serialize; |
||||||
|
|
||||||
|
class PhpHandler implements HandlerInterface { |
||||||
|
/** |
||||||
|
* Simply reverse behaviour of unserialize method. |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
function serialize(array $data) { |
||||||
|
$preSerialized = array(); |
||||||
|
$serialized = ''; |
||||||
|
|
||||||
|
if (count($data)) { |
||||||
|
foreach ($data as $bucket => $bucketData) { |
||||||
|
$preSerialized[] = $bucket . '|' . serialize($bucketData); |
||||||
|
} |
||||||
|
$serialized = implode('', $preSerialized); |
||||||
|
} |
||||||
|
|
||||||
|
return $serialized; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
* @link http://ca2.php.net/manual/en/function.session-decode.php#108037 Code from this comment on php.net |
||||||
|
* @throws \UnexpectedValueException If there is a problem parsing the data |
||||||
|
*/ |
||||||
|
public function unserialize($raw) { |
||||||
|
$returnData = array(); |
||||||
|
$offset = 0; |
||||||
|
|
||||||
|
while ($offset < strlen($raw)) { |
||||||
|
if (!strstr(substr($raw, $offset), "|")) { |
||||||
|
throw new \UnexpectedValueException("invalid data, remaining: " . substr($raw, $offset)); |
||||||
|
} |
||||||
|
|
||||||
|
$pos = strpos($raw, "|", $offset); |
||||||
|
$num = $pos - $offset; |
||||||
|
$varname = substr($raw, $offset, $num); |
||||||
|
$offset += $num + 1; |
||||||
|
$data = unserialize(substr($raw, $offset)); |
||||||
|
|
||||||
|
$returnData[$varname] = $data; |
||||||
|
$offset += strlen(serialize($data)); |
||||||
|
} |
||||||
|
|
||||||
|
return $returnData; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,161 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Session; |
||||||
|
use Ratchet\MessageComponentInterface; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
use Ratchet\WebSocket\WsServerInterface; |
||||||
|
use Ratchet\Session\Storage\VirtualSessionStorage; |
||||||
|
use Ratchet\Session\Serialize\HandlerInterface; |
||||||
|
use Symfony\Component\HttpFoundation\Session\Session; |
||||||
|
use Symfony\Component\HttpFoundation\Session\Storage\Handler\NullSessionHandler; |
||||||
|
|
||||||
|
/** |
||||||
|
* This component will allow access to session data from your website for each user connected |
||||||
|
* Symfony HttpFoundation is required for this component to work |
||||||
|
* Your website must also use Symfony HttpFoundation Sessions to read your sites session data |
||||||
|
* If your are not using at least PHP 5.4 you must include a SessionHandlerInterface stub (is included in Symfony HttpFoundation, loaded w/ composer) |
||||||
|
*/ |
||||||
|
class SessionProvider implements MessageComponentInterface, WsServerInterface { |
||||||
|
/** |
||||||
|
* @var \Ratchet\MessageComponentInterface |
||||||
|
*/ |
||||||
|
protected $_app; |
||||||
|
|
||||||
|
/** |
||||||
|
* Selected handler storage assigned by the developer |
||||||
|
* @var \SessionHandlerInterface |
||||||
|
*/ |
||||||
|
protected $_handler; |
||||||
|
|
||||||
|
/** |
||||||
|
* Null storage handler if no previous session was found |
||||||
|
* @var \SessionHandlerInterface |
||||||
|
*/ |
||||||
|
protected $_null; |
||||||
|
|
||||||
|
/** |
||||||
|
* @var \Ratchet\Session\Serialize\HandlerInterface |
||||||
|
*/ |
||||||
|
protected $_serializer; |
||||||
|
|
||||||
|
/** |
||||||
|
* @param \Ratchet\MessageComponentInterface $app |
||||||
|
* @param \SessionHandlerInterface $handler |
||||||
|
* @param array $options |
||||||
|
* @param \Ratchet\Session\Serialize\HandlerInterface $serializer |
||||||
|
* @throws \RuntimeException |
||||||
|
*/ |
||||||
|
public function __construct(MessageComponentInterface $app, \SessionHandlerInterface $handler, array $options = array(), HandlerInterface $serializer = null) { |
||||||
|
$this->_app = $app; |
||||||
|
$this->_handler = $handler; |
||||||
|
$this->_null = new NullSessionHandler; |
||||||
|
|
||||||
|
ini_set('session.auto_start', 0); |
||||||
|
ini_set('session.cache_limiter', ''); |
||||||
|
ini_set('session.use_cookies', 0); |
||||||
|
|
||||||
|
$this->setOptions($options); |
||||||
|
|
||||||
|
if (null === $serializer) { |
||||||
|
$serialClass = __NAMESPACE__ . "\\Serialize\\{$this->toClassCase(ini_get('session.serialize_handler'))}Handler"; // awesome/terrible hack, eh? |
||||||
|
if (!class_exists($serialClass)) { |
||||||
|
throw new \RuntimeException('Unable to parse session serialize handler'); |
||||||
|
} |
||||||
|
|
||||||
|
$serializer = new $serialClass; |
||||||
|
} |
||||||
|
|
||||||
|
$this->_serializer = $serializer; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
function onOpen(ConnectionInterface $conn) { |
||||||
|
if (!isset($conn->WebSocket) || null === ($id = $conn->WebSocket->request->getCookie(ini_get('session.name')))) { |
||||||
|
$saveHandler = $this->_null; |
||||||
|
$id = ''; |
||||||
|
} else { |
||||||
|
$saveHandler = $this->_handler; |
||||||
|
} |
||||||
|
|
||||||
|
$conn->Session = new Session(new VirtualSessionStorage($saveHandler, $id, $this->_serializer)); |
||||||
|
|
||||||
|
if (ini_get('session.auto_start')) { |
||||||
|
$conn->Session->start(); |
||||||
|
} |
||||||
|
|
||||||
|
return $this->_app->onOpen($conn); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
function onMessage(ConnectionInterface $from, $msg) { |
||||||
|
return $this->_app->onMessage($from, $msg); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
function onClose(ConnectionInterface $conn) { |
||||||
|
// "close" session for Connection |
||||||
|
|
||||||
|
return $this->_app->onClose($conn); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
function onError(ConnectionInterface $conn, \Exception $e) { |
||||||
|
return $this->_app->onError($conn, $e); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getSubProtocols() { |
||||||
|
if ($this->_app instanceof WsServerInterface) { |
||||||
|
return $this->_app->getSubProtocols(); |
||||||
|
} else { |
||||||
|
return array(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Set all the php session. ini options |
||||||
|
* © Symfony |
||||||
|
* @param array $options |
||||||
|
* @return array |
||||||
|
*/ |
||||||
|
protected function setOptions(array $options) { |
||||||
|
$all = array( |
||||||
|
'auto_start', 'cache_limiter', 'cookie_domain', 'cookie_httponly', |
||||||
|
'cookie_lifetime', 'cookie_path', 'cookie_secure', |
||||||
|
'entropy_file', 'entropy_length', 'gc_divisor', |
||||||
|
'gc_maxlifetime', 'gc_probability', 'hash_bits_per_character', |
||||||
|
'hash_function', 'name', 'referer_check', |
||||||
|
'serialize_handler', 'use_cookies', |
||||||
|
'use_only_cookies', 'use_trans_sid', 'upload_progress.enabled', |
||||||
|
'upload_progress.cleanup', 'upload_progress.prefix', 'upload_progress.name', |
||||||
|
'upload_progress.freq', 'upload_progress.min-freq', 'url_rewriter.tags' |
||||||
|
); |
||||||
|
|
||||||
|
foreach ($all as $key) { |
||||||
|
if (!array_key_exists($key, $options)) { |
||||||
|
$options[$key] = ini_get("session.{$key}"); |
||||||
|
} else { |
||||||
|
ini_set("session.{$key}", $options[$key]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return $options; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param string $langDef Input to convert |
||||||
|
* @return string |
||||||
|
*/ |
||||||
|
protected function toClassCase($langDef) { |
||||||
|
return str_replace(' ', '', ucwords(str_replace('_', ' ', $langDef))); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,54 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Session\Storage\Proxy; |
||||||
|
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; |
||||||
|
|
||||||
|
class VirtualProxy extends SessionHandlerProxy { |
||||||
|
/** |
||||||
|
* @var string |
||||||
|
*/ |
||||||
|
protected $_sessionId; |
||||||
|
|
||||||
|
/** |
||||||
|
* @var string |
||||||
|
*/ |
||||||
|
protected $_sessionName; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function __construct(\SessionHandlerInterface $handler) { |
||||||
|
parent::__construct($handler); |
||||||
|
|
||||||
|
$this->saveHandlerName = 'user'; |
||||||
|
$this->_sessionName = ini_get('session.name'); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getId() { |
||||||
|
return $this->_sessionId; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function setId($id) { |
||||||
|
$this->_sessionId = $id; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getName() { |
||||||
|
return $this->_sessionName; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* DO NOT CALL THIS METHOD |
||||||
|
* @internal |
||||||
|
*/ |
||||||
|
public function setName($name) { |
||||||
|
throw new \RuntimeException("Can not change session name in VirtualProxy"); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,88 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Session\Storage; |
||||||
|
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; |
||||||
|
use Ratchet\Session\Storage\Proxy\VirtualProxy; |
||||||
|
use Ratchet\Session\Serialize\HandlerInterface; |
||||||
|
|
||||||
|
class VirtualSessionStorage extends NativeSessionStorage { |
||||||
|
/** |
||||||
|
* @var \Ratchet\Session\Serialize\HandlerInterface |
||||||
|
*/ |
||||||
|
protected $_serializer; |
||||||
|
|
||||||
|
/** |
||||||
|
* @param \SessionHandlerInterface $handler |
||||||
|
* @param string $sessionId The ID of the session to retrieve |
||||||
|
* @param \Ratchet\Session\Serialize\HandlerInterface $serializer |
||||||
|
*/ |
||||||
|
public function __construct(\SessionHandlerInterface $handler, $sessionId, HandlerInterface $serializer) { |
||||||
|
$this->setSaveHandler($handler); |
||||||
|
$this->saveHandler->setId($sessionId); |
||||||
|
$this->_serializer = $serializer; |
||||||
|
$this->setMetadataBag(null); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function start() { |
||||||
|
if ($this->started && !$this->closed) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
// You have to call Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler::open() to use |
||||||
|
// pdo_sqlite (and possible pdo_*) as session storage, if you are using a DSN string instead of a \PDO object |
||||||
|
// in the constructor. The method arguments are filled with the values, which are also used by the symfony |
||||||
|
// framework in this case. This must not be the best choice, but it works. |
||||||
|
$this->saveHandler->open(session_save_path(), session_name()); |
||||||
|
|
||||||
|
$rawData = $this->saveHandler->read($this->saveHandler->getId()); |
||||||
|
$sessionData = $this->_serializer->unserialize($rawData); |
||||||
|
|
||||||
|
$this->loadSession($sessionData); |
||||||
|
|
||||||
|
if (!$this->saveHandler->isWrapper() && !$this->saveHandler->isSessionHandlerInterface()) { |
||||||
|
$this->saveHandler->setActive(false); |
||||||
|
} |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function regenerate($destroy = false, $lifetime = null) { |
||||||
|
// .. ? |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function save() { |
||||||
|
// get the data from the bags? |
||||||
|
// serialize the data |
||||||
|
// save the data using the saveHandler |
||||||
|
// $this->saveHandler->write($this->saveHandler->getId(), |
||||||
|
|
||||||
|
if (!$this->saveHandler->isWrapper() && !$this->getSaveHandler()->isSessionHandlerInterface()) { |
||||||
|
$this->saveHandler->setActive(false); |
||||||
|
} |
||||||
|
|
||||||
|
$this->closed = true; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function setSaveHandler($saveHandler = null) { |
||||||
|
if (!($saveHandler instanceof \SessionHandlerInterface)) { |
||||||
|
throw new \InvalidArgumentException('Handler must be instance of SessionHandlerInterface'); |
||||||
|
} |
||||||
|
|
||||||
|
if (!($saveHandler instanceof VirtualProxy)) { |
||||||
|
$saveHandler = new VirtualProxy($saveHandler); |
||||||
|
} |
||||||
|
|
||||||
|
$this->saveHandler = $saveHandler; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Wamp; |
||||||
|
|
||||||
|
class Exception extends \Exception { |
||||||
|
} |
@ -0,0 +1,31 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Wamp; |
||||||
|
|
||||||
|
class JsonException extends Exception { |
||||||
|
public function __construct() { |
||||||
|
$code = json_last_error(); |
||||||
|
|
||||||
|
switch ($code) { |
||||||
|
case JSON_ERROR_DEPTH: |
||||||
|
$msg = 'Maximum stack depth exceeded'; |
||||||
|
break; |
||||||
|
case JSON_ERROR_STATE_MISMATCH: |
||||||
|
$msg = 'Underflow or the modes mismatch'; |
||||||
|
break; |
||||||
|
case JSON_ERROR_CTRL_CHAR: |
||||||
|
$msg = 'Unexpected control character found'; |
||||||
|
break; |
||||||
|
case JSON_ERROR_SYNTAX: |
||||||
|
$msg = 'Syntax error, malformed JSON'; |
||||||
|
break; |
||||||
|
case JSON_ERROR_UTF8: |
||||||
|
$msg = 'Malformed UTF-8 characters, possibly incorrectly encoded'; |
||||||
|
break; |
||||||
|
default: |
||||||
|
$msg = 'Unknown error'; |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
parent::__construct($msg, $code); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,157 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Wamp; |
||||||
|
use Ratchet\MessageComponentInterface; |
||||||
|
use Ratchet\WebSocket\WsServerInterface; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* WebSocket Application Messaging Protocol |
||||||
|
* |
||||||
|
* @link http://wamp.ws/spec |
||||||
|
* @link https://github.com/oberstet/AutobahnJS |
||||||
|
* |
||||||
|
* +--------------+----+------------------+ |
||||||
|
* | Message Type | ID | DIRECTION | |
||||||
|
* |--------------+----+------------------+ |
||||||
|
* | WELCOME | 0 | Server-to-Client | |
||||||
|
* | PREFIX | 1 | Bi-Directional | |
||||||
|
* | CALL | 2 | Client-to-Server | |
||||||
|
* | CALL RESULT | 3 | Server-to-Client | |
||||||
|
* | CALL ERROR | 4 | Server-to-Client | |
||||||
|
* | SUBSCRIBE | 5 | Client-to-Server | |
||||||
|
* | UNSUBSCRIBE | 6 | Client-to-Server | |
||||||
|
* | PUBLISH | 7 | Client-to-Server | |
||||||
|
* | EVENT | 8 | Server-to-Client | |
||||||
|
* +--------------+----+------------------+ |
||||||
|
*/ |
||||||
|
class ServerProtocol implements MessageComponentInterface, WsServerInterface { |
||||||
|
const MSG_WELCOME = 0; |
||||||
|
const MSG_PREFIX = 1; |
||||||
|
const MSG_CALL = 2; |
||||||
|
const MSG_CALL_RESULT = 3; |
||||||
|
const MSG_CALL_ERROR = 4; |
||||||
|
const MSG_SUBSCRIBE = 5; |
||||||
|
const MSG_UNSUBSCRIBE = 6; |
||||||
|
const MSG_PUBLISH = 7; |
||||||
|
const MSG_EVENT = 8; |
||||||
|
|
||||||
|
/** |
||||||
|
* @var WampServerInterface |
||||||
|
*/ |
||||||
|
protected $_decorating; |
||||||
|
|
||||||
|
/** |
||||||
|
* @var \SplObjectStorage |
||||||
|
*/ |
||||||
|
protected $connections; |
||||||
|
|
||||||
|
/** |
||||||
|
* @param WampServerInterface $serverComponent An class to propagate calls through |
||||||
|
*/ |
||||||
|
public function __construct(WampServerInterface $serverComponent) { |
||||||
|
$this->_decorating = $serverComponent; |
||||||
|
$this->connections = new \SplObjectStorage; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getSubProtocols() { |
||||||
|
if ($this->_decorating instanceof WsServerInterface) { |
||||||
|
$subs = $this->_decorating->getSubProtocols(); |
||||||
|
$subs[] = 'wamp'; |
||||||
|
|
||||||
|
return $subs; |
||||||
|
} else { |
||||||
|
return array('wamp'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onOpen(ConnectionInterface $conn) { |
||||||
|
$decor = new WampConnection($conn); |
||||||
|
$this->connections->attach($conn, $decor); |
||||||
|
|
||||||
|
$this->_decorating->onOpen($decor); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
* @throws \Ratchet\Wamp\Exception |
||||||
|
* @throws \Ratchet\Wamp\JsonException |
||||||
|
*/ |
||||||
|
public function onMessage(ConnectionInterface $from, $msg) { |
||||||
|
$from = $this->connections[$from]; |
||||||
|
|
||||||
|
if (null === ($json = @json_decode($msg, true))) { |
||||||
|
throw new JsonException; |
||||||
|
} |
||||||
|
|
||||||
|
if (!is_array($json) || $json !== array_values($json)) { |
||||||
|
throw new Exception("Invalid WAMP message format"); |
||||||
|
} |
||||||
|
|
||||||
|
switch ($json[0]) { |
||||||
|
case static::MSG_PREFIX: |
||||||
|
$from->WAMP->prefixes[$json[1]] = $json[2]; |
||||||
|
break; |
||||||
|
|
||||||
|
case static::MSG_CALL: |
||||||
|
array_shift($json); |
||||||
|
$callID = array_shift($json); |
||||||
|
$procURI = array_shift($json); |
||||||
|
|
||||||
|
if (count($json) == 1 && is_array($json[0])) { |
||||||
|
$json = $json[0]; |
||||||
|
} |
||||||
|
|
||||||
|
$this->_decorating->onCall($from, $callID, $from->getUri($procURI), $json); |
||||||
|
break; |
||||||
|
|
||||||
|
case static::MSG_SUBSCRIBE: |
||||||
|
$this->_decorating->onSubscribe($from, $from->getUri($json[1])); |
||||||
|
break; |
||||||
|
|
||||||
|
case static::MSG_UNSUBSCRIBE: |
||||||
|
$this->_decorating->onUnSubscribe($from, $from->getUri($json[1])); |
||||||
|
break; |
||||||
|
|
||||||
|
case static::MSG_PUBLISH: |
||||||
|
$exclude = (array_key_exists(3, $json) ? $json[3] : null); |
||||||
|
if (!is_array($exclude)) { |
||||||
|
if (true === (boolean)$exclude) { |
||||||
|
$exclude = array($from->WAMP->sessionId); |
||||||
|
} else { |
||||||
|
$exclude = array(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
$eligible = (array_key_exists(4, $json) ? $json[4] : array()); |
||||||
|
|
||||||
|
$this->_decorating->onPublish($from, $from->getUri($json[1]), $json[2], $exclude, $eligible); |
||||||
|
break; |
||||||
|
|
||||||
|
default: |
||||||
|
throw new Exception('Invalid WAMP message type'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onClose(ConnectionInterface $conn) { |
||||||
|
$decor = $this->connections[$conn]; |
||||||
|
$this->connections->detach($conn); |
||||||
|
|
||||||
|
$this->_decorating->onClose($decor); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onError(ConnectionInterface $conn, \Exception $e) { |
||||||
|
return $this->_decorating->onError($this->connections[$conn], $e); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,106 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Wamp; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* A topic/channel containing connections that have subscribed to it |
||||||
|
*/ |
||||||
|
class Topic implements \IteratorAggregate, \Countable { |
||||||
|
/** |
||||||
|
* If true the TopicManager will destroy this object if it's ever empty of connections |
||||||
|
* @deprecated in v0.4 |
||||||
|
* @type bool |
||||||
|
*/ |
||||||
|
public $autoDelete = false; |
||||||
|
|
||||||
|
private $id; |
||||||
|
|
||||||
|
private $subscribers; |
||||||
|
|
||||||
|
/** |
||||||
|
* @param string $topicId Unique ID for this object |
||||||
|
*/ |
||||||
|
public function __construct($topicId) { |
||||||
|
$this->id = $topicId; |
||||||
|
$this->subscribers = new \SplObjectStorage; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @return string |
||||||
|
*/ |
||||||
|
public function getId() { |
||||||
|
return $this->id; |
||||||
|
} |
||||||
|
|
||||||
|
public function __toString() { |
||||||
|
return $this->getId(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Send a message to all the connections in this topic |
||||||
|
* @param string $msg Payload to publish |
||||||
|
* @param array $exclude A list of session IDs the message should be excluded from (blacklist) |
||||||
|
* @param array $eligible A list of session Ids the message should be send to (whitelist) |
||||||
|
* @return Topic The same Topic object to chain |
||||||
|
*/ |
||||||
|
public function broadcast($msg, array $exclude = array(), array $eligible = array()) { |
||||||
|
$useEligible = (bool)count($eligible); |
||||||
|
foreach ($this->subscribers as $client) { |
||||||
|
if (in_array($client->WAMP->sessionId, $exclude)) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
if ($useEligible && !in_array($client->WAMP->sessionId, $eligible)) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
$client->event($this->id, $msg); |
||||||
|
} |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param WampConnection $conn |
||||||
|
* @return boolean |
||||||
|
*/ |
||||||
|
public function has(ConnectionInterface $conn) { |
||||||
|
return $this->subscribers->contains($conn); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param WampConnection $conn |
||||||
|
* @return Topic |
||||||
|
*/ |
||||||
|
public function add(ConnectionInterface $conn) { |
||||||
|
$this->subscribers->attach($conn); |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param WampConnection $conn |
||||||
|
* @return Topic |
||||||
|
*/ |
||||||
|
public function remove(ConnectionInterface $conn) { |
||||||
|
if ($this->subscribers->contains($conn)) { |
||||||
|
$this->subscribers->detach($conn); |
||||||
|
} |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getIterator() { |
||||||
|
return $this->subscribers; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function count() { |
||||||
|
return $this->subscribers->count(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,125 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Wamp; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
use Ratchet\WebSocket\WsServerInterface; |
||||||
|
|
||||||
|
class TopicManager implements WsServerInterface, WampServerInterface { |
||||||
|
/** |
||||||
|
* @var WampServerInterface |
||||||
|
*/ |
||||||
|
protected $app; |
||||||
|
|
||||||
|
/** |
||||||
|
* @var array |
||||||
|
*/ |
||||||
|
protected $topicLookup = array(); |
||||||
|
|
||||||
|
public function __construct(WampServerInterface $app) { |
||||||
|
$this->app = $app; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onOpen(ConnectionInterface $conn) { |
||||||
|
$conn->WAMP->subscriptions = new \SplObjectStorage; |
||||||
|
$this->app->onOpen($conn); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onCall(ConnectionInterface $conn, $id, $topic, array $params) { |
||||||
|
$this->app->onCall($conn, $id, $this->getTopic($topic), $params); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onSubscribe(ConnectionInterface $conn, $topic) { |
||||||
|
$topicObj = $this->getTopic($topic); |
||||||
|
|
||||||
|
if ($conn->WAMP->subscriptions->contains($topicObj)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
$this->topicLookup[$topic]->add($conn); |
||||||
|
$conn->WAMP->subscriptions->attach($topicObj); |
||||||
|
$this->app->onSubscribe($conn, $topicObj); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onUnsubscribe(ConnectionInterface $conn, $topic) { |
||||||
|
$topicObj = $this->getTopic($topic); |
||||||
|
|
||||||
|
if (!$conn->WAMP->subscriptions->contains($topicObj)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
$this->cleanTopic($topicObj, $conn); |
||||||
|
|
||||||
|
$this->app->onUnsubscribe($conn, $topicObj); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onPublish(ConnectionInterface $conn, $topic, $event, array $exclude, array $eligible) { |
||||||
|
$this->app->onPublish($conn, $this->getTopic($topic), $event, $exclude, $eligible); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onClose(ConnectionInterface $conn) { |
||||||
|
$this->app->onClose($conn); |
||||||
|
|
||||||
|
foreach ($this->topicLookup as $topic) { |
||||||
|
$this->cleanTopic($topic, $conn); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onError(ConnectionInterface $conn, \Exception $e) { |
||||||
|
$this->app->onError($conn, $e); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getSubProtocols() { |
||||||
|
if ($this->app instanceof WsServerInterface) { |
||||||
|
return $this->app->getSubProtocols(); |
||||||
|
} |
||||||
|
|
||||||
|
return array(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param string |
||||||
|
* @return Topic |
||||||
|
*/ |
||||||
|
protected function getTopic($topic) { |
||||||
|
if (!array_key_exists($topic, $this->topicLookup)) { |
||||||
|
$this->topicLookup[$topic] = new Topic($topic); |
||||||
|
} |
||||||
|
|
||||||
|
return $this->topicLookup[$topic]; |
||||||
|
} |
||||||
|
|
||||||
|
protected function cleanTopic(Topic $topic, ConnectionInterface $conn) { |
||||||
|
if ($conn->WAMP->subscriptions->contains($topic)) { |
||||||
|
$conn->WAMP->subscriptions->detach($topic); |
||||||
|
} |
||||||
|
|
||||||
|
$this->topicLookup[$topic->getId()]->remove($conn); |
||||||
|
|
||||||
|
if ($topic->autoDelete && 0 === $topic->count()) { |
||||||
|
unset($this->topicLookup[$topic->getId()]); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,115 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Wamp; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
use Ratchet\AbstractConnectionDecorator; |
||||||
|
use Ratchet\Wamp\ServerProtocol as WAMP; |
||||||
|
|
||||||
|
/** |
||||||
|
* A ConnectionInterface object wrapper that is passed to your WAMP application |
||||||
|
* representing a client. Methods on this Connection are therefore different. |
||||||
|
* @property \stdClass $WAMP |
||||||
|
*/ |
||||||
|
class WampConnection extends AbstractConnectionDecorator { |
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function __construct(ConnectionInterface $conn) { |
||||||
|
parent::__construct($conn); |
||||||
|
|
||||||
|
$this->WAMP = new \StdClass; |
||||||
|
$this->WAMP->sessionId = str_replace('.', '', uniqid(mt_rand(), true)); |
||||||
|
$this->WAMP->prefixes = array(); |
||||||
|
|
||||||
|
$this->send(json_encode(array(WAMP::MSG_WELCOME, $this->WAMP->sessionId, 1, \Ratchet\VERSION))); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Successfully respond to a call made by the client |
||||||
|
* @param string $id The unique ID given by the client to respond to |
||||||
|
* @param array $data an object or array |
||||||
|
* @return WampConnection |
||||||
|
*/ |
||||||
|
public function callResult($id, $data = array()) { |
||||||
|
return $this->send(json_encode(array(WAMP::MSG_CALL_RESULT, $id, $data))); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Respond with an error to a client call |
||||||
|
* @param string $id The unique ID given by the client to respond to |
||||||
|
* @param string $errorUri The URI given to identify the specific error |
||||||
|
* @param string $desc A developer-oriented description of the error |
||||||
|
* @param string $details An optional human readable detail message to send back |
||||||
|
* @return WampConnection |
||||||
|
*/ |
||||||
|
public function callError($id, $errorUri, $desc = '', $details = null) { |
||||||
|
if ($errorUri instanceof Topic) { |
||||||
|
$errorUri = (string)$errorUri; |
||||||
|
} |
||||||
|
|
||||||
|
$data = array(WAMP::MSG_CALL_ERROR, $id, $errorUri, $desc); |
||||||
|
|
||||||
|
if (null !== $details) { |
||||||
|
$data[] = $details; |
||||||
|
} |
||||||
|
|
||||||
|
return $this->send(json_encode($data)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param string $topic The topic to broadcast to |
||||||
|
* @param mixed $msg Data to send with the event. Anything that is json'able |
||||||
|
* @return WampConnection |
||||||
|
*/ |
||||||
|
public function event($topic, $msg) { |
||||||
|
return $this->send(json_encode(array(WAMP::MSG_EVENT, (string)$topic, $msg))); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param string $curie |
||||||
|
* @param string $uri |
||||||
|
* @return WampConnection |
||||||
|
*/ |
||||||
|
public function prefix($curie, $uri) { |
||||||
|
$this->WAMP->prefixes[$curie] = (string)$uri; |
||||||
|
|
||||||
|
return $this->send(json_encode(array(WAMP::MSG_PREFIX, $curie, (string)$uri))); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get the full request URI from the connection object if a prefix has been established for it |
||||||
|
* @param string $uri |
||||||
|
* @return string |
||||||
|
*/ |
||||||
|
public function getUri($uri) { |
||||||
|
$curieSeperator = ':'; |
||||||
|
|
||||||
|
if (preg_match('/http(s*)\:\/\//', $uri) == false) { |
||||||
|
if (strpos($uri, $curieSeperator) !== false) { |
||||||
|
list($prefix, $action) = explode($curieSeperator, $uri); |
||||||
|
|
||||||
|
if(isset($this->WAMP->prefixes[$prefix]) === true){ |
||||||
|
return $this->WAMP->prefixes[$prefix] . '#' . $action; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return $uri; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
*/ |
||||||
|
public function send($data) { |
||||||
|
$this->getConnection()->send($data); |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function close($opt = null) { |
||||||
|
$this->getConnection()->close($opt); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,67 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Wamp; |
||||||
|
use Ratchet\MessageComponentInterface; |
||||||
|
use Ratchet\WebSocket\WsServerInterface; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* Enable support for the official WAMP sub-protocol in your application |
||||||
|
* WAMP allows for Pub/Sub and RPC |
||||||
|
* @link http://wamp.ws The WAMP specification |
||||||
|
* @link https://github.com/oberstet/AutobahnJS Souce for client side library |
||||||
|
* @link http://autobahn.s3.amazonaws.com/js/autobahn.min.js Minified client side library |
||||||
|
*/ |
||||||
|
class WampServer implements MessageComponentInterface, WsServerInterface { |
||||||
|
/** |
||||||
|
* @var ServerProtocol |
||||||
|
*/ |
||||||
|
protected $wampProtocol; |
||||||
|
|
||||||
|
/** |
||||||
|
* This class just makes it 1 step easier to use Topic objects in WAMP |
||||||
|
* If you're looking at the source code, look in the __construct of this |
||||||
|
* class and use that to make your application instead of using this |
||||||
|
*/ |
||||||
|
public function __construct(WampServerInterface $app) { |
||||||
|
$this->wampProtocol = new ServerProtocol(new TopicManager($app)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onOpen(ConnectionInterface $conn) { |
||||||
|
$this->wampProtocol->onOpen($conn); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onMessage(ConnectionInterface $conn, $msg) { |
||||||
|
try { |
||||||
|
$this->wampProtocol->onMessage($conn, $msg); |
||||||
|
} catch (Exception $we) { |
||||||
|
$conn->close(1007); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onClose(ConnectionInterface $conn) { |
||||||
|
$this->wampProtocol->onClose($conn); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onError(ConnectionInterface $conn, \Exception $e) { |
||||||
|
$this->wampProtocol->onError($conn, $e); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getSubProtocols() { |
||||||
|
return $this->wampProtocol->getSubProtocols(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Wamp; |
||||||
|
use Ratchet\ComponentInterface; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* An extension of Ratchet\ComponentInterface to server a WAMP application |
||||||
|
* onMessage is replaced by various types of messages for this protocol (pub/sub or rpc) |
||||||
|
*/ |
||||||
|
interface WampServerInterface extends ComponentInterface { |
||||||
|
/** |
||||||
|
* An RPC call has been received |
||||||
|
* @param \Ratchet\ConnectionInterface $conn |
||||||
|
* @param string $id The unique ID of the RPC, required to respond to |
||||||
|
* @param string|Topic $topic The topic to execute the call against |
||||||
|
* @param array $params Call parameters received from the client |
||||||
|
*/ |
||||||
|
function onCall(ConnectionInterface $conn, $id, $topic, array $params); |
||||||
|
|
||||||
|
/** |
||||||
|
* A request to subscribe to a topic has been made |
||||||
|
* @param \Ratchet\ConnectionInterface $conn |
||||||
|
* @param string|Topic $topic The topic to subscribe to |
||||||
|
*/ |
||||||
|
function onSubscribe(ConnectionInterface $conn, $topic); |
||||||
|
|
||||||
|
/** |
||||||
|
* A request to unsubscribe from a topic has been made |
||||||
|
* @param \Ratchet\ConnectionInterface $conn |
||||||
|
* @param string|Topic $topic The topic to unsubscribe from |
||||||
|
*/ |
||||||
|
function onUnSubscribe(ConnectionInterface $conn, $topic); |
||||||
|
|
||||||
|
/** |
||||||
|
* A client is attempting to publish content to a subscribed connections on a URI |
||||||
|
* @param \Ratchet\ConnectionInterface $conn |
||||||
|
* @param string|Topic $topic The topic the user has attempted to publish to |
||||||
|
* @param string $event Payload of the publish |
||||||
|
* @param array $exclude A list of session IDs the message should be excluded from (blacklist) |
||||||
|
* @param array $eligible A list of session Ids the message should be send to (whitelist) |
||||||
|
*/ |
||||||
|
function onPublish(ConnectionInterface $conn, $topic, $event, array $exclude, array $eligible); |
||||||
|
} |
@ -0,0 +1,31 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\WebSocket\Encoding; |
||||||
|
|
||||||
|
class ToggleableValidator implements ValidatorInterface { |
||||||
|
/** |
||||||
|
* Toggle if checkEncoding checks the encoding or not |
||||||
|
* @var bool |
||||||
|
*/ |
||||||
|
public $on; |
||||||
|
|
||||||
|
/** |
||||||
|
* @var Validator |
||||||
|
*/ |
||||||
|
private $validator; |
||||||
|
|
||||||
|
public function __construct($on = true) { |
||||||
|
$this->validator = new Validator; |
||||||
|
$this->on = (boolean)$on; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function checkEncoding($str, $encoding) { |
||||||
|
if (!(boolean)$this->on) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
return $this->validator->checkEncoding($str, $encoding); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,93 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\WebSocket\Encoding; |
||||||
|
|
||||||
|
/** |
||||||
|
* This class handled encoding validation |
||||||
|
*/ |
||||||
|
class Validator { |
||||||
|
const UTF8_ACCEPT = 0; |
||||||
|
const UTF8_REJECT = 1; |
||||||
|
|
||||||
|
/** |
||||||
|
* Incremental UTF-8 validator with constant memory consumption (minimal state). |
||||||
|
* |
||||||
|
* Implements the algorithm "Flexible and Economical UTF-8 Decoder" by |
||||||
|
* Bjoern Hoehrmann (http://bjoern.hoehrmann.de/utf-8/decoder/dfa/). |
||||||
|
*/ |
||||||
|
protected static $dfa = array( |
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 00..1f |
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 20..3f |
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 40..5f |
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 60..7f |
||||||
|
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, # 80..9f |
||||||
|
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, # a0..bf |
||||||
|
8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, # c0..df |
||||||
|
0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, # e0..ef |
||||||
|
0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, # f0..ff |
||||||
|
0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, # s0..s0 |
||||||
|
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, # s1..s2 |
||||||
|
1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, # s3..s4 |
||||||
|
1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, # s5..s6 |
||||||
|
1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, # s7..s8 |
||||||
|
); |
||||||
|
|
||||||
|
/** |
||||||
|
* Lookup if mbstring is available |
||||||
|
* @var bool |
||||||
|
*/ |
||||||
|
private $hasMbString = false; |
||||||
|
|
||||||
|
/** |
||||||
|
* Lookup if iconv is available |
||||||
|
* @var bool |
||||||
|
*/ |
||||||
|
private $hasIconv = false; |
||||||
|
|
||||||
|
public function __construct() { |
||||||
|
$this->hasMbString = extension_loaded('mbstring'); |
||||||
|
$this->hasIconv = extension_loaded('iconv'); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param string $str The value to check the encoding |
||||||
|
* @param string $against The type of encoding to check against |
||||||
|
* @return bool |
||||||
|
*/ |
||||||
|
public function checkEncoding($str, $against) { |
||||||
|
if ('UTF-8' == $against) { |
||||||
|
return $this->isUtf8($str); |
||||||
|
} |
||||||
|
|
||||||
|
if ($this->hasMbString) { |
||||||
|
return mb_check_encoding($str, $against); |
||||||
|
} elseif ($this->hasIconv) { |
||||||
|
return ($str == iconv($against, "{$against}//IGNORE", $str)); |
||||||
|
} |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
protected function isUtf8($str) { |
||||||
|
if ($this->hasMbString) { |
||||||
|
if (false === mb_check_encoding($str, 'UTF-8')) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} elseif ($this->hasIconv) { |
||||||
|
if ($str != iconv('UTF-8', 'UTF-8//IGNORE', $str)) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
$state = static::UTF8_ACCEPT; |
||||||
|
|
||||||
|
for ($i = 0, $len = strlen($str); $i < $len; $i++) { |
||||||
|
$state = static::$dfa[256 + ($state << 4) + static::$dfa[ord($str[$i])]]; |
||||||
|
|
||||||
|
if (static::UTF8_REJECT === $state) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\WebSocket\Encoding; |
||||||
|
|
||||||
|
interface ValidatorInterface { |
||||||
|
/** |
||||||
|
* Verify a string matches the encoding type |
||||||
|
* @param string $str The string to check |
||||||
|
* @param string $encoding The encoding type to check against |
||||||
|
* @return bool |
||||||
|
*/ |
||||||
|
function checkEncoding($str, $encoding); |
||||||
|
} |
@ -0,0 +1,28 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\WebSocket\Version; |
||||||
|
|
||||||
|
interface DataInterface { |
||||||
|
/** |
||||||
|
* Determine if the message is complete or still fragmented |
||||||
|
* @return bool |
||||||
|
*/ |
||||||
|
function isCoalesced(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Get the number of bytes the payload is set to be |
||||||
|
* @return int |
||||||
|
*/ |
||||||
|
function getPayloadLength(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Get the payload (message) sent from peer |
||||||
|
* @return string |
||||||
|
*/ |
||||||
|
function getPayload(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Get raw contents of the message |
||||||
|
* @return string |
||||||
|
*/ |
||||||
|
function getContents(); |
||||||
|
} |
@ -0,0 +1,38 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\WebSocket\Version; |
||||||
|
|
||||||
|
interface FrameInterface extends DataInterface { |
||||||
|
/** |
||||||
|
* Add incoming data to the frame from peer |
||||||
|
* @param string |
||||||
|
*/ |
||||||
|
function addBuffer($buf); |
||||||
|
|
||||||
|
/** |
||||||
|
* Is this the final frame in a fragmented message? |
||||||
|
* @return bool |
||||||
|
*/ |
||||||
|
function isFinal(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Is the payload masked? |
||||||
|
* @return bool |
||||||
|
*/ |
||||||
|
function isMasked(); |
||||||
|
|
||||||
|
/** |
||||||
|
* @return int |
||||||
|
*/ |
||||||
|
function getOpcode(); |
||||||
|
|
||||||
|
/** |
||||||
|
* @return int |
||||||
|
*/ |
||||||
|
//function getReceivedPayloadLength(); |
||||||
|
|
||||||
|
/** |
||||||
|
* 32-big string |
||||||
|
* @return string |
||||||
|
*/ |
||||||
|
function getMaskingKey(); |
||||||
|
} |
@ -0,0 +1,120 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\WebSocket\Version; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
use Ratchet\MessageInterface; |
||||||
|
use Ratchet\WebSocket\Version\Hixie76\Connection; |
||||||
|
use Guzzle\Http\Message\RequestInterface; |
||||||
|
use Guzzle\Http\Message\Response; |
||||||
|
use Ratchet\WebSocket\Version\Hixie76\Frame; |
||||||
|
|
||||||
|
/** |
||||||
|
* FOR THE LOVE OF BEER, PLEASE PLEASE PLEASE DON'T allow the use of this in your application! |
||||||
|
* Hixie76 is bad for 2 (there's more) reasons: |
||||||
|
* 1) The handshake is done in HTTP, which includes a key for signing in the body... |
||||||
|
* BUT there is no Length defined in the header (as per HTTP spec) so the TCP buffer can't tell when the message is done! |
||||||
|
* 2) By nature it's insecure. Google did a test study where they were able to do a |
||||||
|
* man-in-the-middle attack on 10%-15% of the people who saw their ad who had a browser (currently only Safari) supporting the Hixie76 protocol. |
||||||
|
* This was exploited by taking advantage of proxy servers in front of the user who ignored some HTTP headers in the handshake |
||||||
|
* The Hixie76 is currently implemented by Safari |
||||||
|
* @link http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 |
||||||
|
*/ |
||||||
|
class Hixie76 implements VersionInterface { |
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function isProtocol(RequestInterface $request) { |
||||||
|
return !(null === $request->getHeader('Sec-WebSocket-Key2')); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getVersionNumber() { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param \Guzzle\Http\Message\RequestInterface $request |
||||||
|
* @return \Guzzle\Http\Message\Response |
||||||
|
* @throws \UnderflowException If there hasn't been enough data received |
||||||
|
*/ |
||||||
|
public function handshake(RequestInterface $request) { |
||||||
|
$body = substr($request->getBody(), 0, 8); |
||||||
|
if (8 !== strlen($body)) { |
||||||
|
throw new \UnderflowException("Not enough data received to issue challenge response"); |
||||||
|
} |
||||||
|
|
||||||
|
$challenge = $this->sign((string)$request->getHeader('Sec-WebSocket-Key1'), (string)$request->getHeader('Sec-WebSocket-Key2'), $body); |
||||||
|
|
||||||
|
$headers = array( |
||||||
|
'Upgrade' => 'WebSocket' |
||||||
|
, 'Connection' => 'Upgrade' |
||||||
|
, 'Sec-WebSocket-Origin' => (string)$request->getHeader('Origin') |
||||||
|
, 'Sec-WebSocket-Location' => 'ws://' . (string)$request->getHeader('Host') . $request->getPath() |
||||||
|
); |
||||||
|
|
||||||
|
$response = new Response(101, $headers, $challenge); |
||||||
|
$response->setStatus(101, 'WebSocket Protocol Handshake'); |
||||||
|
|
||||||
|
return $response; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function upgradeConnection(ConnectionInterface $conn, MessageInterface $coalescedCallback) { |
||||||
|
$upgraded = new Connection($conn); |
||||||
|
|
||||||
|
if (!isset($upgraded->WebSocket)) { |
||||||
|
$upgraded->WebSocket = new \StdClass; |
||||||
|
} |
||||||
|
|
||||||
|
$upgraded->WebSocket->coalescedCallback = $coalescedCallback; |
||||||
|
|
||||||
|
return $upgraded; |
||||||
|
} |
||||||
|
|
||||||
|
public function onMessage(ConnectionInterface $from, $data) { |
||||||
|
$overflow = ''; |
||||||
|
|
||||||
|
if (!isset($from->WebSocket->frame)) { |
||||||
|
$from->WebSocket->frame = $this->newFrame(); |
||||||
|
} |
||||||
|
|
||||||
|
$from->WebSocket->frame->addBuffer($data); |
||||||
|
if ($from->WebSocket->frame->isCoalesced()) { |
||||||
|
$overflow = $from->WebSocket->frame->extractOverflow(); |
||||||
|
|
||||||
|
$parsed = $from->WebSocket->frame->getPayload(); |
||||||
|
unset($from->WebSocket->frame); |
||||||
|
|
||||||
|
$from->WebSocket->coalescedCallback->onMessage($from, $parsed); |
||||||
|
|
||||||
|
unset($from->WebSocket->frame); |
||||||
|
} |
||||||
|
|
||||||
|
if (strlen($overflow) > 0) { |
||||||
|
$this->onMessage($from, $overflow); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public function newFrame() { |
||||||
|
return new Frame; |
||||||
|
} |
||||||
|
|
||||||
|
public function generateKeyNumber($key) { |
||||||
|
if (0 === substr_count($key, ' ')) { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
return preg_replace('[\D]', '', $key) / substr_count($key, ' '); |
||||||
|
} |
||||||
|
|
||||||
|
protected function sign($key1, $key2, $code) { |
||||||
|
return md5( |
||||||
|
pack('N', $this->generateKeyNumber($key1)) |
||||||
|
. pack('N', $this->generateKeyNumber($key2)) |
||||||
|
. $code |
||||||
|
, true); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\WebSocket\Version\Hixie76; |
||||||
|
use Ratchet\AbstractConnectionDecorator; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
* @property \StdClass $WebSocket |
||||||
|
*/ |
||||||
|
class Connection extends AbstractConnectionDecorator { |
||||||
|
public function send($msg) { |
||||||
|
if (!$this->WebSocket->closing) { |
||||||
|
$this->getConnection()->send(chr(0) . $msg . chr(255)); |
||||||
|
} |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function close() { |
||||||
|
if (!$this->WebSocket->closing) { |
||||||
|
$this->getConnection()->send(chr(255)); |
||||||
|
$this->getConnection()->close(); |
||||||
|
|
||||||
|
$this->WebSocket->closing = true; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,86 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\WebSocket\Version\Hixie76; |
||||||
|
use Ratchet\WebSocket\Version\FrameInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* This does not entirely follow the protocol to spec, but (mostly) works |
||||||
|
* Hixie76 probably should not even be supported |
||||||
|
*/ |
||||||
|
class Frame implements FrameInterface { |
||||||
|
/** |
||||||
|
* @type string |
||||||
|
*/ |
||||||
|
protected $_data = ''; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function isCoalesced() { |
||||||
|
return (boolean)($this->_data[0] == chr(0) && substr($this->_data, -1) == chr(255)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function addBuffer($buf) { |
||||||
|
$this->_data .= (string)$buf; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function isFinal() { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function isMasked() { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getOpcode() { |
||||||
|
return 1; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getPayloadLength() { |
||||||
|
if (!$this->isCoalesced()) { |
||||||
|
throw new \UnderflowException('Not enough of the message has been buffered to determine the length of the payload'); |
||||||
|
} |
||||||
|
|
||||||
|
return strlen($this->_data) - 2; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getMaskingKey() { |
||||||
|
return ''; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getPayload() { |
||||||
|
if (!$this->isCoalesced()) { |
||||||
|
return new \UnderflowException('Not enough data buffered to read payload'); |
||||||
|
} |
||||||
|
|
||||||
|
return substr($this->_data, 1, strlen($this->_data) - 2); |
||||||
|
} |
||||||
|
|
||||||
|
public function getContents() { |
||||||
|
return $this->_data; |
||||||
|
} |
||||||
|
|
||||||
|
public function extractOverflow() { |
||||||
|
return ''; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\WebSocket\Version; |
||||||
|
use Guzzle\Http\Message\RequestInterface; |
||||||
|
|
||||||
|
class HyBi10 extends RFC6455 { |
||||||
|
public function isProtocol(RequestInterface $request) { |
||||||
|
$version = (int)(string)$request->getHeader('Sec-WebSocket-Version'); |
||||||
|
|
||||||
|
return ($version >= 6 && $version < 13); |
||||||
|
} |
||||||
|
|
||||||
|
public function getVersionNumber() { |
||||||
|
return 6; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\WebSocket\Version; |
||||||
|
|
||||||
|
interface MessageInterface extends DataInterface { |
||||||
|
/** |
||||||
|
* @param FrameInterface $fragment |
||||||
|
* @return MessageInterface |
||||||
|
*/ |
||||||
|
function addFrame(FrameInterface $fragment); |
||||||
|
|
||||||
|
/** |
||||||
|
* @return int |
||||||
|
*/ |
||||||
|
function getOpcode(); |
||||||
|
} |
@ -0,0 +1,273 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\WebSocket\Version; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
use Ratchet\MessageInterface; |
||||||
|
use Ratchet\WebSocket\Version\RFC6455\HandshakeVerifier; |
||||||
|
use Ratchet\WebSocket\Version\RFC6455\Message; |
||||||
|
use Ratchet\WebSocket\Version\RFC6455\Frame; |
||||||
|
use Ratchet\WebSocket\Version\RFC6455\Connection; |
||||||
|
use Ratchet\WebSocket\Encoding\ValidatorInterface; |
||||||
|
use Ratchet\WebSocket\Encoding\Validator; |
||||||
|
use Guzzle\Http\Message\RequestInterface; |
||||||
|
use Guzzle\Http\Message\Response; |
||||||
|
|
||||||
|
/** |
||||||
|
* The latest version of the WebSocket protocol |
||||||
|
* @link http://tools.ietf.org/html/rfc6455 |
||||||
|
* @todo Unicode: return mb_convert_encoding(pack("N",$u), mb_internal_encoding(), 'UCS-4BE'); |
||||||
|
*/ |
||||||
|
class RFC6455 implements VersionInterface { |
||||||
|
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; |
||||||
|
|
||||||
|
/** |
||||||
|
* @var RFC6455\HandshakeVerifier |
||||||
|
*/ |
||||||
|
protected $_verifier; |
||||||
|
|
||||||
|
/** |
||||||
|
* A lookup of the valid close codes that can be sent in a frame |
||||||
|
* @var array |
||||||
|
*/ |
||||||
|
private $closeCodes = array(); |
||||||
|
|
||||||
|
/** |
||||||
|
* @var \Ratchet\WebSocket\Encoding\ValidatorInterface |
||||||
|
*/ |
||||||
|
protected $validator; |
||||||
|
|
||||||
|
public function __construct(ValidatorInterface $validator = null) { |
||||||
|
$this->_verifier = new HandshakeVerifier; |
||||||
|
$this->setCloseCodes(); |
||||||
|
|
||||||
|
if (null === $validator) { |
||||||
|
$validator = new Validator; |
||||||
|
} |
||||||
|
|
||||||
|
$this->validator = $validator; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function isProtocol(RequestInterface $request) { |
||||||
|
$version = (int)(string)$request->getHeader('Sec-WebSocket-Version'); |
||||||
|
|
||||||
|
return ($this->getVersionNumber() === $version); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getVersionNumber() { |
||||||
|
return 13; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function handshake(RequestInterface $request) { |
||||||
|
if (true !== $this->_verifier->verifyAll($request)) { |
||||||
|
return new Response(400); |
||||||
|
} |
||||||
|
|
||||||
|
return new Response(101, array( |
||||||
|
'Upgrade' => 'websocket' |
||||||
|
, 'Connection' => 'Upgrade' |
||||||
|
, 'Sec-WebSocket-Accept' => $this->sign((string)$request->getHeader('Sec-WebSocket-Key')) |
||||||
|
)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param \Ratchet\ConnectionInterface $conn |
||||||
|
* @param \Ratchet\MessageInterface $coalescedCallback |
||||||
|
* @return \Ratchet\WebSocket\Version\RFC6455\Connection |
||||||
|
*/ |
||||||
|
public function upgradeConnection(ConnectionInterface $conn, MessageInterface $coalescedCallback) { |
||||||
|
$upgraded = new Connection($conn); |
||||||
|
|
||||||
|
if (!isset($upgraded->WebSocket)) { |
||||||
|
$upgraded->WebSocket = new \StdClass; |
||||||
|
} |
||||||
|
|
||||||
|
$upgraded->WebSocket->coalescedCallback = $coalescedCallback; |
||||||
|
|
||||||
|
return $upgraded; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param \Ratchet\WebSocket\Version\RFC6455\Connection $from |
||||||
|
* @param string $data |
||||||
|
*/ |
||||||
|
public function onMessage(ConnectionInterface $from, $data) { |
||||||
|
$overflow = ''; |
||||||
|
|
||||||
|
if (!isset($from->WebSocket->message)) { |
||||||
|
$from->WebSocket->message = $this->newMessage(); |
||||||
|
} |
||||||
|
|
||||||
|
// There is a frame fragment attached to the connection, add to it |
||||||
|
if (!isset($from->WebSocket->frame)) { |
||||||
|
$from->WebSocket->frame = $this->newFrame(); |
||||||
|
} |
||||||
|
|
||||||
|
$from->WebSocket->frame->addBuffer($data); |
||||||
|
if ($from->WebSocket->frame->isCoalesced()) { |
||||||
|
$frame = $from->WebSocket->frame; |
||||||
|
|
||||||
|
if (false !== $frame->getRsv1() || |
||||||
|
false !== $frame->getRsv2() || |
||||||
|
false !== $frame->getRsv3() |
||||||
|
) { |
||||||
|
return $from->close($frame::CLOSE_PROTOCOL); |
||||||
|
} |
||||||
|
|
||||||
|
if (!$frame->isMasked()) { |
||||||
|
return $from->close($frame::CLOSE_PROTOCOL); |
||||||
|
} |
||||||
|
|
||||||
|
$opcode = $frame->getOpcode(); |
||||||
|
|
||||||
|
if ($opcode > 2) { |
||||||
|
if ($frame->getPayloadLength() > 125 || !$frame->isFinal()) { |
||||||
|
return $from->close($frame::CLOSE_PROTOCOL); |
||||||
|
} |
||||||
|
|
||||||
|
switch ($opcode) { |
||||||
|
case $frame::OP_CLOSE: |
||||||
|
$closeCode = 0; |
||||||
|
|
||||||
|
$bin = $frame->getPayload(); |
||||||
|
|
||||||
|
if (empty($bin)) { |
||||||
|
return $from->close(); |
||||||
|
} |
||||||
|
|
||||||
|
if (strlen($bin) >= 2) { |
||||||
|
list($closeCode) = array_merge(unpack('n*', substr($bin, 0, 2))); |
||||||
|
} |
||||||
|
|
||||||
|
if (!$this->isValidCloseCode($closeCode)) { |
||||||
|
return $from->close($frame::CLOSE_PROTOCOL); |
||||||
|
} |
||||||
|
|
||||||
|
if (!$this->validator->checkEncoding(substr($bin, 2), 'UTF-8')) { |
||||||
|
return $from->close($frame::CLOSE_BAD_PAYLOAD); |
||||||
|
} |
||||||
|
|
||||||
|
$frame->unMaskPayload(); |
||||||
|
|
||||||
|
return $from->close($frame); |
||||||
|
break; |
||||||
|
case $frame::OP_PING: |
||||||
|
$from->send($this->newFrame($frame->getPayload(), true, $frame::OP_PONG)); |
||||||
|
break; |
||||||
|
case $frame::OP_PONG: |
||||||
|
break; |
||||||
|
default: |
||||||
|
return $from->close($frame::CLOSE_PROTOCOL); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
$overflow = $from->WebSocket->frame->extractOverflow(); |
||||||
|
|
||||||
|
unset($from->WebSocket->frame, $frame, $opcode); |
||||||
|
|
||||||
|
if (strlen($overflow) > 0) { |
||||||
|
$this->onMessage($from, $overflow); |
||||||
|
} |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
$overflow = $from->WebSocket->frame->extractOverflow(); |
||||||
|
|
||||||
|
if ($frame::OP_CONTINUE == $frame->getOpcode() && 0 == count($from->WebSocket->message)) { |
||||||
|
return $from->close($frame::CLOSE_PROTOCOL); |
||||||
|
} |
||||||
|
|
||||||
|
if (count($from->WebSocket->message) > 0 && $frame::OP_CONTINUE != $frame->getOpcode()) { |
||||||
|
return $from->close($frame::CLOSE_PROTOCOL); |
||||||
|
} |
||||||
|
|
||||||
|
$from->WebSocket->message->addFrame($from->WebSocket->frame); |
||||||
|
unset($from->WebSocket->frame); |
||||||
|
} |
||||||
|
|
||||||
|
if ($from->WebSocket->message->isCoalesced()) { |
||||||
|
$parsed = $from->WebSocket->message->getPayload(); |
||||||
|
unset($from->WebSocket->message); |
||||||
|
|
||||||
|
if (!$this->validator->checkEncoding($parsed, 'UTF-8')) { |
||||||
|
return $from->close(Frame::CLOSE_BAD_PAYLOAD); |
||||||
|
} |
||||||
|
|
||||||
|
$from->WebSocket->coalescedCallback->onMessage($from, $parsed); |
||||||
|
} |
||||||
|
|
||||||
|
if (strlen($overflow) > 0) { |
||||||
|
$this->onMessage($from, $overflow); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @return RFC6455\Message |
||||||
|
*/ |
||||||
|
public function newMessage() { |
||||||
|
return new Message; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param string|null $payload |
||||||
|
* @param bool|null $final |
||||||
|
* @param int|null $opcode |
||||||
|
* @return RFC6455\Frame |
||||||
|
*/ |
||||||
|
public function newFrame($payload = null, $final = null, $opcode = null) { |
||||||
|
return new Frame($payload, $final, $opcode); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Used when doing the handshake to encode the key, verifying client/server are speaking the same language |
||||||
|
* @param string $key |
||||||
|
* @return string |
||||||
|
* @internal |
||||||
|
*/ |
||||||
|
public function sign($key) { |
||||||
|
return base64_encode(sha1($key . static::GUID, true)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Determine if a close code is valid |
||||||
|
* @param int|string |
||||||
|
* @return bool |
||||||
|
*/ |
||||||
|
public function isValidCloseCode($val) { |
||||||
|
if (array_key_exists($val, $this->closeCodes)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
if ($val >= 3000 && $val <= 4999) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a private lookup of valid, private close codes |
||||||
|
*/ |
||||||
|
protected function setCloseCodes() { |
||||||
|
$this->closeCodes[Frame::CLOSE_NORMAL] = true; |
||||||
|
$this->closeCodes[Frame::CLOSE_GOING_AWAY] = true; |
||||||
|
$this->closeCodes[Frame::CLOSE_PROTOCOL] = true; |
||||||
|
$this->closeCodes[Frame::CLOSE_BAD_DATA] = true; |
||||||
|
//$this->closeCodes[Frame::CLOSE_NO_STATUS] = true; |
||||||
|
//$this->closeCodes[Frame::CLOSE_ABNORMAL] = true; |
||||||
|
$this->closeCodes[Frame::CLOSE_BAD_PAYLOAD] = true; |
||||||
|
$this->closeCodes[Frame::CLOSE_POLICY] = true; |
||||||
|
$this->closeCodes[Frame::CLOSE_TOO_BIG] = true; |
||||||
|
$this->closeCodes[Frame::CLOSE_MAND_EXT] = true; |
||||||
|
$this->closeCodes[Frame::CLOSE_SRV_ERR] = true; |
||||||
|
//$this->closeCodes[Frame::CLOSE_TLS] = true; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,44 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\WebSocket\Version\RFC6455; |
||||||
|
use Ratchet\AbstractConnectionDecorator; |
||||||
|
use Ratchet\WebSocket\Version\DataInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
* @property \StdClass $WebSocket |
||||||
|
*/ |
||||||
|
class Connection extends AbstractConnectionDecorator { |
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function send($msg) { |
||||||
|
if (!$this->WebSocket->closing) { |
||||||
|
if (!($msg instanceof DataInterface)) { |
||||||
|
$msg = new Frame($msg); |
||||||
|
} |
||||||
|
|
||||||
|
$this->getConnection()->send($msg->getContents()); |
||||||
|
} |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function close($code = 1000) { |
||||||
|
if ($this->WebSocket->closing) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if ($code instanceof DataInterface) { |
||||||
|
$this->send($code); |
||||||
|
} else { |
||||||
|
$this->send(new Frame(pack('n', $code), true, Frame::OP_CLOSE)); |
||||||
|
} |
||||||
|
|
||||||
|
$this->getConnection()->close(); |
||||||
|
|
||||||
|
$this->WebSocket->closing = true; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,451 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\WebSocket\Version\RFC6455; |
||||||
|
use Ratchet\WebSocket\Version\FrameInterface; |
||||||
|
|
||||||
|
class Frame implements FrameInterface { |
||||||
|
const OP_CONTINUE = 0; |
||||||
|
const OP_TEXT = 1; |
||||||
|
const OP_BINARY = 2; |
||||||
|
const OP_CLOSE = 8; |
||||||
|
const OP_PING = 9; |
||||||
|
const OP_PONG = 10; |
||||||
|
|
||||||
|
const CLOSE_NORMAL = 1000; |
||||||
|
const CLOSE_GOING_AWAY = 1001; |
||||||
|
const CLOSE_PROTOCOL = 1002; |
||||||
|
const CLOSE_BAD_DATA = 1003; |
||||||
|
const CLOSE_NO_STATUS = 1005; |
||||||
|
const CLOSE_ABNORMAL = 1006; |
||||||
|
const CLOSE_BAD_PAYLOAD = 1007; |
||||||
|
const CLOSE_POLICY = 1008; |
||||||
|
const CLOSE_TOO_BIG = 1009; |
||||||
|
const CLOSE_MAND_EXT = 1010; |
||||||
|
const CLOSE_SRV_ERR = 1011; |
||||||
|
const CLOSE_TLS = 1015; |
||||||
|
|
||||||
|
const MASK_LENGTH = 4; |
||||||
|
|
||||||
|
/** |
||||||
|
* The contents of the frame |
||||||
|
* @var string |
||||||
|
*/ |
||||||
|
protected $data = ''; |
||||||
|
|
||||||
|
/** |
||||||
|
* Number of bytes received from the frame |
||||||
|
* @var int |
||||||
|
*/ |
||||||
|
public $bytesRecvd = 0; |
||||||
|
|
||||||
|
/** |
||||||
|
* Number of bytes in the payload (as per framing protocol) |
||||||
|
* @var int |
||||||
|
*/ |
||||||
|
protected $defPayLen = -1; |
||||||
|
|
||||||
|
/** |
||||||
|
* If the frame is coalesced this is true |
||||||
|
* This is to prevent doing math every time ::isCoalesced is called |
||||||
|
* @var boolean |
||||||
|
*/ |
||||||
|
private $isCoalesced = false; |
||||||
|
|
||||||
|
/** |
||||||
|
* The unpacked first byte of the frame |
||||||
|
* @var int |
||||||
|
*/ |
||||||
|
protected $firstByte = -1; |
||||||
|
|
||||||
|
/** |
||||||
|
* The unpacked second byte of the frame |
||||||
|
* @var int |
||||||
|
*/ |
||||||
|
protected $secondByte = -1; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* @param string|null $payload |
||||||
|
* @param bool $final |
||||||
|
* @param int $opcode |
||||||
|
*/ |
||||||
|
public function __construct($payload = null, $final = true, $opcode = 1) { |
||||||
|
if (null === $payload) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
$this->defPayLen = strlen($payload); |
||||||
|
$this->firstByte = ($final ? 128 : 0) + $opcode; |
||||||
|
$this->secondByte = $this->defPayLen; |
||||||
|
$this->isCoalesced = true; |
||||||
|
|
||||||
|
$ext = ''; |
||||||
|
if ($this->defPayLen > 65535) { |
||||||
|
$ext = pack('NN', 0, $this->defPayLen); |
||||||
|
$this->secondByte = 127; |
||||||
|
} elseif ($this->defPayLen > 125) { |
||||||
|
$ext = pack('n', $this->defPayLen); |
||||||
|
$this->secondByte = 126; |
||||||
|
} |
||||||
|
|
||||||
|
$this->data = chr($this->firstByte) . chr($this->secondByte) . $ext . $payload; |
||||||
|
$this->bytesRecvd = 2 + strlen($ext) + $this->defPayLen; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function isCoalesced() { |
||||||
|
if (true === $this->isCoalesced) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
$payload_length = $this->getPayloadLength(); |
||||||
|
$payload_start = $this->getPayloadStartingByte(); |
||||||
|
} catch (\UnderflowException $e) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
$this->isCoalesced = $this->bytesRecvd >= $payload_length + $payload_start; |
||||||
|
|
||||||
|
return $this->isCoalesced; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function addBuffer($buf) { |
||||||
|
$len = strlen($buf); |
||||||
|
|
||||||
|
$this->data .= $buf; |
||||||
|
$this->bytesRecvd += $len; |
||||||
|
|
||||||
|
if ($this->firstByte === -1 && $this->bytesRecvd !== 0) { |
||||||
|
$this->firstByte = ord($this->data[0]); |
||||||
|
} |
||||||
|
|
||||||
|
if ($this->secondByte === -1 && $this->bytesRecvd >= 2) { |
||||||
|
$this->secondByte = ord($this->data[1]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function isFinal() { |
||||||
|
if (-1 === $this->firstByte) { |
||||||
|
throw new \UnderflowException('Not enough bytes received to determine if this is the final frame in message'); |
||||||
|
} |
||||||
|
|
||||||
|
return 128 === ($this->firstByte & 128); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @return boolean |
||||||
|
* @throws \UnderflowException |
||||||
|
*/ |
||||||
|
public function getRsv1() { |
||||||
|
if (-1 === $this->firstByte) { |
||||||
|
throw new \UnderflowException('Not enough bytes received to determine reserved bit'); |
||||||
|
} |
||||||
|
|
||||||
|
return 64 === ($this->firstByte & 64); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @return boolean |
||||||
|
* @throws \UnderflowException |
||||||
|
*/ |
||||||
|
public function getRsv2() { |
||||||
|
if (-1 === $this->firstByte) { |
||||||
|
throw new \UnderflowException('Not enough bytes received to determine reserved bit'); |
||||||
|
} |
||||||
|
|
||||||
|
return 32 === ($this->firstByte & 32); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @return boolean |
||||||
|
* @throws \UnderflowException |
||||||
|
*/ |
||||||
|
public function getRsv3() { |
||||||
|
if (-1 === $this->firstByte) { |
||||||
|
throw new \UnderflowException('Not enough bytes received to determine reserved bit'); |
||||||
|
} |
||||||
|
|
||||||
|
return 16 == ($this->firstByte & 16); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function isMasked() { |
||||||
|
if (-1 === $this->secondByte) { |
||||||
|
throw new \UnderflowException("Not enough bytes received ({$this->bytesRecvd}) to determine if mask is set"); |
||||||
|
} |
||||||
|
|
||||||
|
return 128 === ($this->secondByte & 128); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getMaskingKey() { |
||||||
|
if (!$this->isMasked()) { |
||||||
|
return ''; |
||||||
|
} |
||||||
|
|
||||||
|
$start = 1 + $this->getNumPayloadBytes(); |
||||||
|
|
||||||
|
if ($this->bytesRecvd < $start + static::MASK_LENGTH) { |
||||||
|
throw new \UnderflowException('Not enough data buffered to calculate the masking key'); |
||||||
|
} |
||||||
|
|
||||||
|
return substr($this->data, $start, static::MASK_LENGTH); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a 4 byte masking key |
||||||
|
* @return string |
||||||
|
*/ |
||||||
|
public function generateMaskingKey() { |
||||||
|
$mask = ''; |
||||||
|
|
||||||
|
for ($i = 1; $i <= static::MASK_LENGTH; $i++) { |
||||||
|
$mask .= chr(rand(32, 126)); |
||||||
|
} |
||||||
|
|
||||||
|
return $mask; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Apply a mask to the payload |
||||||
|
* @param string|null If NULL is passed a masking key will be generated |
||||||
|
* @throws \OutOfBoundsException |
||||||
|
* @throws \InvalidArgumentException If there is an issue with the given masking key |
||||||
|
* @return Frame |
||||||
|
*/ |
||||||
|
public function maskPayload($maskingKey = null) { |
||||||
|
if (null === $maskingKey) { |
||||||
|
$maskingKey = $this->generateMaskingKey(); |
||||||
|
} |
||||||
|
|
||||||
|
if (static::MASK_LENGTH !== strlen($maskingKey)) { |
||||||
|
throw new \InvalidArgumentException("Masking key must be " . static::MASK_LENGTH ." characters"); |
||||||
|
} |
||||||
|
|
||||||
|
if (extension_loaded('mbstring') && true !== mb_check_encoding($maskingKey, 'US-ASCII')) { |
||||||
|
throw new \OutOfBoundsException("Masking key MUST be ASCII"); |
||||||
|
} |
||||||
|
|
||||||
|
$this->unMaskPayload(); |
||||||
|
|
||||||
|
$this->secondByte = $this->secondByte | 128; |
||||||
|
$this->data[1] = chr($this->secondByte); |
||||||
|
|
||||||
|
$this->data = substr_replace($this->data, $maskingKey, $this->getNumPayloadBytes() + 1, 0); |
||||||
|
|
||||||
|
$this->bytesRecvd += static::MASK_LENGTH; |
||||||
|
$this->data = substr_replace($this->data, $this->applyMask($maskingKey), $this->getPayloadStartingByte(), $this->getPayloadLength()); |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Remove a mask from the payload |
||||||
|
* @throws \UnderFlowException If the frame is not coalesced |
||||||
|
* @return Frame |
||||||
|
*/ |
||||||
|
public function unMaskPayload() { |
||||||
|
if (!$this->isCoalesced()) { |
||||||
|
throw new \UnderflowException('Frame must be coalesced before applying mask'); |
||||||
|
} |
||||||
|
|
||||||
|
if (!$this->isMasked()) { |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
$maskingKey = $this->getMaskingKey(); |
||||||
|
|
||||||
|
$this->secondByte = $this->secondByte & ~128; |
||||||
|
$this->data[1] = chr($this->secondByte); |
||||||
|
|
||||||
|
$this->data = substr_replace($this->data, '', $this->getNumPayloadBytes() + 1, static::MASK_LENGTH); |
||||||
|
|
||||||
|
$this->bytesRecvd -= static::MASK_LENGTH; |
||||||
|
$this->data = substr_replace($this->data, $this->applyMask($maskingKey), $this->getPayloadStartingByte(), $this->getPayloadLength()); |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Apply a mask to a string or the payload of the instance |
||||||
|
* @param string $maskingKey The 4 character masking key to be applied |
||||||
|
* @param string|null $payload A string to mask or null to use the payload |
||||||
|
* @throws \UnderflowException If using the payload but enough hasn't been buffered |
||||||
|
* @return string The masked string |
||||||
|
*/ |
||||||
|
public function applyMask($maskingKey, $payload = null) { |
||||||
|
if (null === $payload) { |
||||||
|
if (!$this->isCoalesced()) { |
||||||
|
throw new \UnderflowException('Frame must be coalesced to apply a mask'); |
||||||
|
} |
||||||
|
|
||||||
|
$payload = substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength()); |
||||||
|
} |
||||||
|
|
||||||
|
$applied = ''; |
||||||
|
for ($i = 0, $len = strlen($payload); $i < $len; $i++) { |
||||||
|
$applied .= $payload[$i] ^ $maskingKey[$i % static::MASK_LENGTH]; |
||||||
|
} |
||||||
|
|
||||||
|
return $applied; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getOpcode() { |
||||||
|
if (-1 === $this->firstByte) { |
||||||
|
throw new \UnderflowException('Not enough bytes received to determine opcode'); |
||||||
|
} |
||||||
|
|
||||||
|
return ($this->firstByte & ~240); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Gets the decimal value of bits 9 (10th) through 15 inclusive |
||||||
|
* @return int |
||||||
|
* @throws \UnderflowException If the buffer doesn't have enough data to determine this |
||||||
|
*/ |
||||||
|
protected function getFirstPayloadVal() { |
||||||
|
if (-1 === $this->secondByte) { |
||||||
|
throw new \UnderflowException('Not enough bytes received'); |
||||||
|
} |
||||||
|
|
||||||
|
return $this->secondByte & 127; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @return int (7|23|71) Number of bits defined for the payload length in the fame |
||||||
|
* @throws \UnderflowException |
||||||
|
*/ |
||||||
|
protected function getNumPayloadBits() { |
||||||
|
if (-1 === $this->secondByte) { |
||||||
|
throw new \UnderflowException('Not enough bytes received'); |
||||||
|
} |
||||||
|
|
||||||
|
// By default 7 bits are used to describe the payload length |
||||||
|
// These are bits 9 (10th) through 15 inclusive |
||||||
|
$bits = 7; |
||||||
|
|
||||||
|
// Get the value of those bits |
||||||
|
$check = $this->getFirstPayloadVal(); |
||||||
|
|
||||||
|
// If the value is 126 the 7 bits plus the next 16 are used to describe the payload length |
||||||
|
if ($check >= 126) { |
||||||
|
$bits += 16; |
||||||
|
} |
||||||
|
|
||||||
|
// If the value of the initial payload length are is 127 an additional 48 bits are used to describe length |
||||||
|
// Note: The documentation specifies the length is to be 63 bits, but I think that's a typo and is 64 (16+48) |
||||||
|
if ($check === 127) { |
||||||
|
$bits += 48; |
||||||
|
} |
||||||
|
|
||||||
|
return $bits; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* This just returns the number of bytes used in the frame to describe the payload length (as opposed to # of bits) |
||||||
|
* @see getNumPayloadBits |
||||||
|
*/ |
||||||
|
protected function getNumPayloadBytes() { |
||||||
|
return (1 + $this->getNumPayloadBits()) / 8; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getPayloadLength() { |
||||||
|
if ($this->defPayLen !== -1) { |
||||||
|
return $this->defPayLen; |
||||||
|
} |
||||||
|
|
||||||
|
$this->defPayLen = $this->getFirstPayloadVal(); |
||||||
|
if ($this->defPayLen <= 125) { |
||||||
|
return $this->getPayloadLength(); |
||||||
|
} |
||||||
|
|
||||||
|
$byte_length = $this->getNumPayloadBytes(); |
||||||
|
if ($this->bytesRecvd < 1 + $byte_length) { |
||||||
|
$this->defPayLen = -1; |
||||||
|
throw new \UnderflowException('Not enough data buffered to determine payload length'); |
||||||
|
} |
||||||
|
|
||||||
|
$len = 0; |
||||||
|
for ($i = 2; $i <= $byte_length; $i++) { |
||||||
|
$len <<= 8; |
||||||
|
$len += ord($this->data[$i]); |
||||||
|
} |
||||||
|
|
||||||
|
$this->defPayLen = $len; |
||||||
|
|
||||||
|
return $this->getPayloadLength(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getPayloadStartingByte() { |
||||||
|
return 1 + $this->getNumPayloadBytes() + ($this->isMasked() ? static::MASK_LENGTH : 0); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
* @todo Consider not checking mask, always returning the payload, masked or not |
||||||
|
*/ |
||||||
|
public function getPayload() { |
||||||
|
if (!$this->isCoalesced()) { |
||||||
|
throw new \UnderflowException('Can not return partial message'); |
||||||
|
} |
||||||
|
|
||||||
|
$payload = substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength()); |
||||||
|
|
||||||
|
if ($this->isMasked()) { |
||||||
|
$payload = $this->applyMask($this->getMaskingKey(), $payload); |
||||||
|
} |
||||||
|
|
||||||
|
return $payload; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get the raw contents of the frame |
||||||
|
* @todo This is untested, make sure the substr is right - trying to return the frame w/o the overflow |
||||||
|
*/ |
||||||
|
public function getContents() { |
||||||
|
return substr($this->data, 0, $this->getPayloadStartingByte() + $this->getPayloadLength()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Sometimes clients will concatenate more than one frame over the wire |
||||||
|
* This method will take the extra bytes off the end and return them |
||||||
|
* @todo Consider returning new Frame |
||||||
|
* @return string |
||||||
|
*/ |
||||||
|
public function extractOverflow() { |
||||||
|
if ($this->isCoalesced()) { |
||||||
|
$endPoint = $this->getPayloadLength(); |
||||||
|
$endPoint += $this->getPayloadStartingByte(); |
||||||
|
|
||||||
|
if ($this->bytesRecvd > $endPoint) { |
||||||
|
$overflow = substr($this->data, $endPoint); |
||||||
|
$this->data = substr($this->data, 0, $endPoint); |
||||||
|
|
||||||
|
return $overflow; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return ''; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,137 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\WebSocket\Version\RFC6455; |
||||||
|
use Guzzle\Http\Message\RequestInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* These are checks to ensure the client requested handshake are valid |
||||||
|
* Verification rules come from section 4.2.1 of the RFC6455 document |
||||||
|
* @todo Currently just returning invalid - should consider returning appropriate HTTP status code error #s |
||||||
|
*/ |
||||||
|
class HandshakeVerifier { |
||||||
|
/** |
||||||
|
* Given an array of the headers this method will run through all verification methods |
||||||
|
* @param \Guzzle\Http\Message\RequestInterface $request |
||||||
|
* @return bool TRUE if all headers are valid, FALSE if 1 or more were invalid |
||||||
|
*/ |
||||||
|
public function verifyAll(RequestInterface $request) { |
||||||
|
$passes = 0; |
||||||
|
|
||||||
|
$passes += (int)$this->verifyMethod($request->getMethod()); |
||||||
|
$passes += (int)$this->verifyHTTPVersion($request->getProtocolVersion()); |
||||||
|
$passes += (int)$this->verifyRequestURI($request->getPath()); |
||||||
|
$passes += (int)$this->verifyHost((string)$request->getHeader('Host')); |
||||||
|
$passes += (int)$this->verifyUpgradeRequest((string)$request->getHeader('Upgrade')); |
||||||
|
$passes += (int)$this->verifyConnection((string)$request->getHeader('Connection')); |
||||||
|
$passes += (int)$this->verifyKey((string)$request->getHeader('Sec-WebSocket-Key')); |
||||||
|
//$passes += (int)$this->verifyVersion($headers['Sec-WebSocket-Version']); // Temporarily breaking functionality |
||||||
|
|
||||||
|
return (7 === $passes); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Test the HTTP method. MUST be "GET" |
||||||
|
* @param string |
||||||
|
* @return bool |
||||||
|
*/ |
||||||
|
public function verifyMethod($val) { |
||||||
|
return ('get' === strtolower($val)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Test the HTTP version passed. MUST be 1.1 or greater |
||||||
|
* @param string|int |
||||||
|
* @return bool |
||||||
|
*/ |
||||||
|
public function verifyHTTPVersion($val) { |
||||||
|
return (1.1 <= (double)$val); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param string |
||||||
|
* @return bool |
||||||
|
*/ |
||||||
|
public function verifyRequestURI($val) { |
||||||
|
if ($val[0] != '/') { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
if (false !== strstr($val, '#')) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
if (!extension_loaded('mbstring')) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
return mb_check_encoding($val, 'US-ASCII'); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param string|null |
||||||
|
* @return bool |
||||||
|
* @todo Find out if I can find the master socket, ensure the port is attached to header if not 80 or 443 - not sure if this is possible, as I tried to hide it |
||||||
|
* @todo Once I fix HTTP::getHeaders just verify this isn't NULL or empty...or maybe need to verify it's a valid domain??? Or should it equal $_SERVER['HOST'] ? |
||||||
|
*/ |
||||||
|
public function verifyHost($val) { |
||||||
|
return (null !== $val); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Verify the Upgrade request to WebSockets. |
||||||
|
* @param string $val MUST equal "websocket" |
||||||
|
* @return bool |
||||||
|
*/ |
||||||
|
public function verifyUpgradeRequest($val) { |
||||||
|
return ('websocket' === strtolower($val)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Verify the Connection header |
||||||
|
* @param string $val MUST equal "Upgrade" |
||||||
|
* @return bool |
||||||
|
*/ |
||||||
|
public function verifyConnection($val) { |
||||||
|
$val = strtolower($val); |
||||||
|
|
||||||
|
if ('upgrade' === $val) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
$vals = explode(',', str_replace(', ', ',', $val)); |
||||||
|
|
||||||
|
return (false !== array_search('upgrade', $vals)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* This function verifies the nonce is valid (64 big encoded, 16 bytes random string) |
||||||
|
* @param string|null |
||||||
|
* @return bool |
||||||
|
* @todo The spec says we don't need to base64_decode - can I just check if the length is 24 and not decode? |
||||||
|
* @todo Check the spec to see what the encoding of the key could be |
||||||
|
*/ |
||||||
|
public function verifyKey($val) { |
||||||
|
return (16 === strlen(base64_decode((string)$val))); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Verify the version passed matches this RFC |
||||||
|
* @param string|int MUST equal 13|"13" |
||||||
|
* @return bool |
||||||
|
* @todo Ran in to a problem here...I'm having HyBi use the RFC files, this breaks it! oops |
||||||
|
*/ |
||||||
|
public function verifyVersion($val) { |
||||||
|
return (13 === (int)$val); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @todo Write logic for this method. See section 4.2.1.8 |
||||||
|
*/ |
||||||
|
public function verifyProtocol($val) { |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @todo Write logic for this method. See section 4.2.1.9 |
||||||
|
*/ |
||||||
|
public function verifyExtensions($val) { |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,107 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\WebSocket\Version\RFC6455; |
||||||
|
use Ratchet\WebSocket\Version\MessageInterface; |
||||||
|
use Ratchet\WebSocket\Version\FrameInterface; |
||||||
|
|
||||||
|
class Message implements MessageInterface, \Countable { |
||||||
|
/** |
||||||
|
* @var \SplDoublyLinkedList |
||||||
|
*/ |
||||||
|
protected $_frames; |
||||||
|
|
||||||
|
public function __construct() { |
||||||
|
$this->_frames = new \SplDoublyLinkedList; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function count() { |
||||||
|
return count($this->_frames); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function isCoalesced() { |
||||||
|
if (count($this->_frames) == 0) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
$last = $this->_frames->top(); |
||||||
|
|
||||||
|
return ($last->isCoalesced() && $last->isFinal()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
* @todo Also, I should perhaps check the type...control frames (ping/pong/close) are not to be considered part of a message |
||||||
|
*/ |
||||||
|
public function addFrame(FrameInterface $fragment) { |
||||||
|
$this->_frames->push($fragment); |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getOpcode() { |
||||||
|
if (count($this->_frames) == 0) { |
||||||
|
throw new \UnderflowException('No frames have been added to this message'); |
||||||
|
} |
||||||
|
|
||||||
|
return $this->_frames->bottom()->getOpcode(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getPayloadLength() { |
||||||
|
$len = 0; |
||||||
|
|
||||||
|
foreach ($this->_frames as $frame) { |
||||||
|
try { |
||||||
|
$len += $frame->getPayloadLength(); |
||||||
|
} catch (\UnderflowException $e) { |
||||||
|
// Not an error, want the current amount buffered |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return $len; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getPayload() { |
||||||
|
if (!$this->isCoalesced()) { |
||||||
|
throw new \UnderflowException('Message has not been put back together yet'); |
||||||
|
} |
||||||
|
|
||||||
|
$buffer = ''; |
||||||
|
|
||||||
|
foreach ($this->_frames as $frame) { |
||||||
|
$buffer .= $frame->getPayload(); |
||||||
|
} |
||||||
|
|
||||||
|
return $buffer; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getContents() { |
||||||
|
if (!$this->isCoalesced()) { |
||||||
|
throw new \UnderflowException("Message has not been put back together yet"); |
||||||
|
} |
||||||
|
|
||||||
|
$buffer = ''; |
||||||
|
|
||||||
|
foreach ($this->_frames as $frame) { |
||||||
|
$buffer .= $frame->getContents(); |
||||||
|
} |
||||||
|
|
||||||
|
return $buffer; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,57 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\WebSocket\Version; |
||||||
|
use Ratchet\MessageInterface; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
use Guzzle\Http\Message\RequestInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* A standard interface for interacting with the various version of the WebSocket protocol |
||||||
|
*/ |
||||||
|
interface VersionInterface extends MessageInterface { |
||||||
|
/** |
||||||
|
* Given an HTTP header, determine if this version should handle the protocol |
||||||
|
* @param \Guzzle\Http\Message\RequestInterface $request |
||||||
|
* @return bool |
||||||
|
* @throws \UnderflowException If the protocol thinks the headers are still fragmented |
||||||
|
*/ |
||||||
|
function isProtocol(RequestInterface $request); |
||||||
|
|
||||||
|
/** |
||||||
|
* Although the version has a name associated with it the integer returned is the proper identification |
||||||
|
* @return int |
||||||
|
*/ |
||||||
|
function getVersionNumber(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Perform the handshake and return the response headers |
||||||
|
* @param \Guzzle\Http\Message\RequestInterface $request |
||||||
|
* @return \Guzzle\Http\Message\Response |
||||||
|
* @throws \UnderflowException If the message hasn't finished buffering (not yet implemented, theoretically will only happen with Hixie version) |
||||||
|
*/ |
||||||
|
function handshake(RequestInterface $request); |
||||||
|
|
||||||
|
/** |
||||||
|
* @param \Ratchet\ConnectionInterface $conn |
||||||
|
* @param \Ratchet\MessageInterface $coalescedCallback |
||||||
|
* @return \Ratchet\ConnectionInterface |
||||||
|
*/ |
||||||
|
function upgradeConnection(ConnectionInterface $conn, MessageInterface $coalescedCallback); |
||||||
|
|
||||||
|
/** |
||||||
|
* @return MessageInterface |
||||||
|
*/ |
||||||
|
//function newMessage(); |
||||||
|
|
||||||
|
/** |
||||||
|
* @return FrameInterface |
||||||
|
*/ |
||||||
|
//function newFrame(); |
||||||
|
|
||||||
|
/** |
||||||
|
* @param string |
||||||
|
* @param bool |
||||||
|
* @return string |
||||||
|
* @todo Change to use other classes, this will be removed eventually |
||||||
|
*/ |
||||||
|
//function frame($message, $mask = true); |
||||||
|
} |
@ -0,0 +1,90 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\WebSocket; |
||||||
|
use Ratchet\WebSocket\Version\VersionInterface; |
||||||
|
use Guzzle\Http\Message\RequestInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* Manage the various versions of the WebSocket protocol |
||||||
|
* This accepts interfaces of versions to enable/disable |
||||||
|
*/ |
||||||
|
class VersionManager { |
||||||
|
/** |
||||||
|
* The header string to let clients know which versions are supported |
||||||
|
* @var string |
||||||
|
*/ |
||||||
|
private $versionString = ''; |
||||||
|
|
||||||
|
/** |
||||||
|
* Storage of each version enabled |
||||||
|
* @var array |
||||||
|
*/ |
||||||
|
protected $versions = array(); |
||||||
|
|
||||||
|
/** |
||||||
|
* Get the protocol negotiator for the request, if supported |
||||||
|
* @param \Guzzle\Http\Message\RequestInterface $request |
||||||
|
* @throws \InvalidArgumentException |
||||||
|
* @return \Ratchet\WebSocket\Version\VersionInterface |
||||||
|
*/ |
||||||
|
public function getVersion(RequestInterface $request) { |
||||||
|
foreach ($this->versions as $version) { |
||||||
|
if ($version->isProtocol($request)) { |
||||||
|
return $version; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
throw new \InvalidArgumentException("Version not found"); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param \Guzzle\Http\Message\RequestInterface |
||||||
|
* @return bool |
||||||
|
*/ |
||||||
|
public function isVersionEnabled(RequestInterface $request) { |
||||||
|
foreach ($this->versions as $version) { |
||||||
|
if ($version->isProtocol($request)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Enable support for a specific version of the WebSocket protocol |
||||||
|
* @param \Ratchet\WebSocket\Version\VersionInterface $version |
||||||
|
* @return VersionManager |
||||||
|
*/ |
||||||
|
public function enableVersion(VersionInterface $version) { |
||||||
|
$this->versions[$version->getVersionNumber()] = $version; |
||||||
|
|
||||||
|
if (empty($this->versionString)) { |
||||||
|
$this->versionString = (string)$version->getVersionNumber(); |
||||||
|
} else { |
||||||
|
$this->versionString .= ", {$version->getVersionNumber()}"; |
||||||
|
} |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Disable support for a specific WebSocket protocol version |
||||||
|
* @param int $versionId The version ID to un-support |
||||||
|
* @return VersionManager |
||||||
|
*/ |
||||||
|
public function disableVersion($versionId) { |
||||||
|
unset($this->versions[$versionId]); |
||||||
|
|
||||||
|
$this->versionString = implode(',', array_keys($this->versions)); |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get a string of version numbers supported (comma delimited) |
||||||
|
* @return string |
||||||
|
*/ |
||||||
|
public function getSupportedVersionString() { |
||||||
|
return $this->versionString; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,232 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\WebSocket; |
||||||
|
use Ratchet\MessageComponentInterface; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
use Ratchet\Http\HttpServerInterface; |
||||||
|
use Guzzle\Http\Message\RequestInterface; |
||||||
|
use Guzzle\Http\Message\Response; |
||||||
|
use Ratchet\WebSocket\Version; |
||||||
|
use Ratchet\WebSocket\Encoding\ToggleableValidator; |
||||||
|
|
||||||
|
/** |
||||||
|
* The adapter to handle WebSocket requests/responses |
||||||
|
* This is a mediator between the Server and your application to handle real-time messaging through a web browser |
||||||
|
* @link http://ca.php.net/manual/en/ref.http.php |
||||||
|
* @link http://dev.w3.org/html5/websockets/ |
||||||
|
*/ |
||||||
|
class WsServer implements HttpServerInterface { |
||||||
|
/** |
||||||
|
* Manage the various WebSocket versions to support |
||||||
|
* @var VersionManager |
||||||
|
* @note May not expose this in the future, may do through facade methods |
||||||
|
*/ |
||||||
|
public $versioner; |
||||||
|
|
||||||
|
/** |
||||||
|
* Decorated component |
||||||
|
* @var \Ratchet\MessageComponentInterface |
||||||
|
*/ |
||||||
|
public $component; |
||||||
|
|
||||||
|
/** |
||||||
|
* @var \SplObjectStorage |
||||||
|
*/ |
||||||
|
protected $connections; |
||||||
|
|
||||||
|
/** |
||||||
|
* Holder of accepted protocols, implement through WampServerInterface |
||||||
|
*/ |
||||||
|
protected $acceptedSubProtocols = array(); |
||||||
|
|
||||||
|
/** |
||||||
|
* UTF-8 validator |
||||||
|
* @var \Ratchet\WebSocket\Encoding\ValidatorInterface |
||||||
|
*/ |
||||||
|
protected $validator; |
||||||
|
|
||||||
|
/** |
||||||
|
* Flag if we have checked the decorated component for sub-protocols |
||||||
|
* @var boolean |
||||||
|
*/ |
||||||
|
private $isSpGenerated = false; |
||||||
|
|
||||||
|
/** |
||||||
|
* @param \Ratchet\MessageComponentInterface $component Your application to run with WebSockets |
||||||
|
* If you want to enable sub-protocols have your component implement WsServerInterface as well |
||||||
|
*/ |
||||||
|
public function __construct(MessageComponentInterface $component) { |
||||||
|
$this->versioner = new VersionManager; |
||||||
|
$this->validator = new ToggleableValidator; |
||||||
|
|
||||||
|
$this->versioner |
||||||
|
->enableVersion(new Version\RFC6455($this->validator)) |
||||||
|
->enableVersion(new Version\HyBi10($this->validator)) |
||||||
|
->enableVersion(new Version\Hixie76) |
||||||
|
; |
||||||
|
|
||||||
|
$this->component = $component; |
||||||
|
$this->connections = new \SplObjectStorage; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) { |
||||||
|
if (null === $request) { |
||||||
|
throw new \UnexpectedValueException('$request can not be null'); |
||||||
|
} |
||||||
|
|
||||||
|
$conn->WebSocket = new \StdClass; |
||||||
|
$conn->WebSocket->request = $request; |
||||||
|
$conn->WebSocket->established = false; |
||||||
|
$conn->WebSocket->closing = false; |
||||||
|
|
||||||
|
$this->attemptUpgrade($conn); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onMessage(ConnectionInterface $from, $msg) { |
||||||
|
if ($from->WebSocket->closing) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (true === $from->WebSocket->established) { |
||||||
|
return $from->WebSocket->version->onMessage($this->connections[$from], $msg); |
||||||
|
} |
||||||
|
|
||||||
|
$this->attemptUpgrade($from, $msg); |
||||||
|
} |
||||||
|
|
||||||
|
protected function attemptUpgrade(ConnectionInterface $conn, $data = '') { |
||||||
|
if ('' !== $data) { |
||||||
|
$conn->WebSocket->request->getBody()->write($data); |
||||||
|
} |
||||||
|
|
||||||
|
if (!$this->versioner->isVersionEnabled($conn->WebSocket->request)) { |
||||||
|
return $this->close($conn); |
||||||
|
} |
||||||
|
|
||||||
|
$conn->WebSocket->version = $this->versioner->getVersion($conn->WebSocket->request); |
||||||
|
|
||||||
|
try { |
||||||
|
$response = $conn->WebSocket->version->handshake($conn->WebSocket->request); |
||||||
|
} catch (\UnderflowException $e) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (null !== ($subHeader = $conn->WebSocket->request->getHeader('Sec-WebSocket-Protocol'))) { |
||||||
|
if ('' !== ($agreedSubProtocols = $this->getSubProtocolString($subHeader->normalize()))) { |
||||||
|
$response->setHeader('Sec-WebSocket-Protocol', $agreedSubProtocols); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
$response->setHeader('X-Powered-By', \Ratchet\VERSION); |
||||||
|
$conn->send((string)$response); |
||||||
|
|
||||||
|
if (101 != $response->getStatusCode()) { |
||||||
|
return $conn->close(); |
||||||
|
} |
||||||
|
|
||||||
|
$upgraded = $conn->WebSocket->version->upgradeConnection($conn, $this->component); |
||||||
|
|
||||||
|
$this->connections->attach($conn, $upgraded); |
||||||
|
|
||||||
|
$upgraded->WebSocket->established = true; |
||||||
|
|
||||||
|
return $this->component->onOpen($upgraded); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onClose(ConnectionInterface $conn) { |
||||||
|
if ($this->connections->contains($conn)) { |
||||||
|
$decor = $this->connections[$conn]; |
||||||
|
$this->connections->detach($conn); |
||||||
|
|
||||||
|
$this->component->onClose($decor); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function onError(ConnectionInterface $conn, \Exception $e) { |
||||||
|
if ($conn->WebSocket->established && $this->connections->contains($conn)) { |
||||||
|
$this->component->onError($this->connections[$conn], $e); |
||||||
|
} else { |
||||||
|
$conn->close(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Disable a specific version of the WebSocket protocol |
||||||
|
* @param int $versionId Version ID to disable |
||||||
|
* @return WsServer |
||||||
|
*/ |
||||||
|
public function disableVersion($versionId) { |
||||||
|
$this->versioner->disableVersion($versionId); |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Toggle weather to check encoding of incoming messages |
||||||
|
* @param bool |
||||||
|
* @return WsServer |
||||||
|
*/ |
||||||
|
public function setEncodingChecks($opt) { |
||||||
|
$this->validator->on = (boolean)$opt; |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param string |
||||||
|
* @return boolean |
||||||
|
*/ |
||||||
|
public function isSubProtocolSupported($name) { |
||||||
|
if (!$this->isSpGenerated) { |
||||||
|
if ($this->component instanceof WsServerInterface) { |
||||||
|
$this->acceptedSubProtocols = array_flip($this->component->getSubProtocols()); |
||||||
|
} |
||||||
|
|
||||||
|
$this->isSpGenerated = true; |
||||||
|
} |
||||||
|
|
||||||
|
return array_key_exists($name, $this->acceptedSubProtocols); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param \Traversable|null $requested |
||||||
|
* @return string |
||||||
|
*/ |
||||||
|
protected function getSubProtocolString(\Traversable $requested = null) { |
||||||
|
if (null !== $requested) { |
||||||
|
foreach ($requested as $sub) { |
||||||
|
if ($this->isSubProtocolSupported($sub)) { |
||||||
|
return $sub; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return ''; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Close a connection with an HTTP response |
||||||
|
* @param \Ratchet\ConnectionInterface $conn |
||||||
|
* @param int $code HTTP status code |
||||||
|
*/ |
||||||
|
protected function close(ConnectionInterface $conn, $code = 400) { |
||||||
|
$response = new Response($code, array( |
||||||
|
'Sec-WebSocket-Version' => $this->versioner->getSupportedVersionString() |
||||||
|
, 'X-Powered-By' => \Ratchet\VERSION |
||||||
|
)); |
||||||
|
|
||||||
|
$conn->send((string)$response); |
||||||
|
$conn->close(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\WebSocket; |
||||||
|
|
||||||
|
/** |
||||||
|
* WebSocket Server Interface |
||||||
|
*/ |
||||||
|
interface WsServerInterface { |
||||||
|
/** |
||||||
|
* If any component in a stack supports a WebSocket sub-protocol return each supported in an array |
||||||
|
* @return array |
||||||
|
* @todo This method may be removed in future version (note that will not break code, just make some code obsolete) |
||||||
|
*/ |
||||||
|
function getSubProtocols(); |
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
require dirname(dirname(dirname(__DIR__))) . '/vendor/autoload.php'; |
||||||
|
|
||||||
|
$port = $argc > 1 ? $argv[1] : 8000; |
||||||
|
$impl = sprintf('React\EventLoop\%sLoop', $argc > 2 ? $argv[2] : 'StreamSelect'); |
||||||
|
|
||||||
|
$loop = new $impl; |
||||||
|
$sock = new React\Socket\Server($loop); |
||||||
|
$web = new Ratchet\WebSocket\WsServer(new Ratchet\Server\EchoServer); |
||||||
|
$app = new Ratchet\Http\HttpServer($web); |
||||||
|
$web->setEncodingChecks(false); |
||||||
|
|
||||||
|
$sock->listen($port, '0.0.0.0'); |
||||||
|
|
||||||
|
$server = new Ratchet\Server\IoServer($app, $sock, $loop); |
||||||
|
$server->run(); |
@ -0,0 +1,15 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
require dirname(dirname(dirname(__DIR__))) . '/vendor/autoload.php'; |
||||||
|
|
||||||
|
$port = $argc > 1 ? $argv[1] : 8000; |
||||||
|
$impl = sprintf('React\EventLoop\%sLoop', $argc > 2 ? $argv[2] : 'StreamSelect'); |
||||||
|
|
||||||
|
$loop = new $impl; |
||||||
|
$sock = new React\Socket\Server($loop); |
||||||
|
$app = new Ratchet\Http\HttpServer(new Ratchet\WebSocket\WsServer(new Ratchet\Server\EchoServer)); |
||||||
|
|
||||||
|
$sock->listen($port, '0.0.0.0'); |
||||||
|
|
||||||
|
$server = new Ratchet\Server\IoServer($app, $sock, $loop); |
||||||
|
$server->run(); |
@ -0,0 +1,16 @@ |
|||||||
|
{ |
||||||
|
"options": {"failByDrop": false} |
||||||
|
, "outdir": "reports/ab" |
||||||
|
|
||||||
|
, "servers": [ |
||||||
|
{"agent": "Ratchet/0.3 libevent", "url": "ws://localhost:8001", "options": {"version": 18}} |
||||||
|
, {"agent": "Ratchet/0.3 libev", "url": "ws://localhost:8004", "options": {"version": 18}} |
||||||
|
, {"agent": "Ratchet/0.3 streams", "url": "ws://localhost:8002", "options": {"version": 18}} |
||||||
|
, {"agent": "Ratchet/0.3 -utf8", "url": "ws://localhost:8003", "options": {"version": 18}} |
||||||
|
, {"agent": "AutobahnTestSuite/0.5.9", "url": "ws://localhost:8000", "options": {"version": 18}} |
||||||
|
] |
||||||
|
|
||||||
|
, "cases": ["*"] |
||||||
|
, "exclude-cases": ["1.2.*", "2.3", "2.4", "2.6", "9.2.*", "9.4.*", "9.6.*", "9.8.*"] |
||||||
|
, "exclude-agent-cases": {} |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
{ |
||||||
|
"options": {"failByDrop": false} |
||||||
|
, "outdir": "reports/profile" |
||||||
|
|
||||||
|
, "servers": [ |
||||||
|
{"agent": "Ratchet", "url": "ws://localhost:8000", "options": {"version": 18}} |
||||||
|
] |
||||||
|
|
||||||
|
, "cases": ["9.7.4"] |
||||||
|
, "exclude-cases": ["1.2.*", "2.3", "2.4", "2.6", "9.2.*", "9.4.*", "9.6.*", "9.8.*"] |
||||||
|
, "exclude-agent-cases": {} |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
{ |
||||||
|
"options": {"failByDrop": false} |
||||||
|
, "outdir": "reports/rfc" |
||||||
|
|
||||||
|
, "servers": [ |
||||||
|
{"agent": "Ratchet", "url": "ws://localhost:8000", "options": {"version": 18}} |
||||||
|
] |
||||||
|
|
||||||
|
, "cases": ["*"] |
||||||
|
, "exclude-cases": ["1.2.*", "2.3", "2.4", "2.6", "9.2.*", "9.4.*", "9.6.*", "9.8.*"] |
||||||
|
, "exclude-agent-cases": {} |
||||||
|
} |
@ -0,0 +1,4 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
$loader = require __DIR__ . '/../vendor/autoload.php'; |
||||||
|
$loader->addPsr4('Ratchet\\', __DIR__ . '/helpers/Ratchet'); |
@ -0,0 +1,50 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet; |
||||||
|
|
||||||
|
abstract class AbstractMessageComponentTestCase extends \PHPUnit_Framework_TestCase { |
||||||
|
protected $_app; |
||||||
|
protected $_serv; |
||||||
|
protected $_conn; |
||||||
|
|
||||||
|
abstract public function getConnectionClassString(); |
||||||
|
abstract public function getDecoratorClassString(); |
||||||
|
abstract public function getComponentClassString(); |
||||||
|
|
||||||
|
public function setUp() { |
||||||
|
$this->_app = $this->getMock($this->getComponentClassString()); |
||||||
|
$decorator = $this->getDecoratorClassString(); |
||||||
|
$this->_serv = new $decorator($this->_app); |
||||||
|
$this->_conn = $this->getMock('\Ratchet\ConnectionInterface'); |
||||||
|
|
||||||
|
$this->doOpen($this->_conn); |
||||||
|
} |
||||||
|
|
||||||
|
protected function doOpen($conn) { |
||||||
|
$this->_serv->onOpen($conn); |
||||||
|
} |
||||||
|
|
||||||
|
public function isExpectedConnection() { |
||||||
|
return new \PHPUnit_Framework_Constraint_IsInstanceOf($this->getConnectionClassString()); |
||||||
|
} |
||||||
|
|
||||||
|
public function testOpen() { |
||||||
|
$this->_app->expects($this->once())->method('onOpen')->with($this->isExpectedConnection()); |
||||||
|
$this->doOpen($this->getMock('\Ratchet\ConnectionInterface')); |
||||||
|
} |
||||||
|
|
||||||
|
public function testOnClose() { |
||||||
|
$this->_app->expects($this->once())->method('onClose')->with($this->isExpectedConnection()); |
||||||
|
$this->_serv->onClose($this->_conn); |
||||||
|
} |
||||||
|
|
||||||
|
public function testOnError() { |
||||||
|
$e = new \Exception('Whoops!'); |
||||||
|
$this->_app->expects($this->once())->method('onError')->with($this->isExpectedConnection(), $e); |
||||||
|
$this->_serv->onError($this->_conn, $e); |
||||||
|
} |
||||||
|
|
||||||
|
public function passthroughMessageTest($value) { |
||||||
|
$this->_app->expects($this->once())->method('onMessage')->with($this->isExpectedConnection(), $value); |
||||||
|
$this->_serv->onMessage($this->_conn, $value); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Mock; |
||||||
|
use Ratchet\MessageComponentInterface; |
||||||
|
use Ratchet\WebSocket\WsServerInterface; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
|
||||||
|
class Component implements MessageComponentInterface, WsServerInterface { |
||||||
|
public $last = array(); |
||||||
|
|
||||||
|
public $protocols = array(); |
||||||
|
|
||||||
|
public function __construct(ComponentInterface $app = null) { |
||||||
|
$this->last[__FUNCTION__] = func_get_args(); |
||||||
|
} |
||||||
|
|
||||||
|
public function onOpen(ConnectionInterface $conn) { |
||||||
|
$this->last[__FUNCTION__] = func_get_args(); |
||||||
|
} |
||||||
|
|
||||||
|
public function onMessage(ConnectionInterface $from, $msg) { |
||||||
|
$this->last[__FUNCTION__] = func_get_args(); |
||||||
|
} |
||||||
|
|
||||||
|
public function onClose(ConnectionInterface $conn) { |
||||||
|
$this->last[__FUNCTION__] = func_get_args(); |
||||||
|
} |
||||||
|
|
||||||
|
public function onError(ConnectionInterface $conn, \Exception $e) { |
||||||
|
$this->last[__FUNCTION__] = func_get_args(); |
||||||
|
} |
||||||
|
|
||||||
|
public function getSubProtocols() { |
||||||
|
return $this->protocols; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Mock; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
|
||||||
|
class Connection implements ConnectionInterface { |
||||||
|
public $last = array( |
||||||
|
'send' => '' |
||||||
|
, 'close' => false |
||||||
|
); |
||||||
|
|
||||||
|
public $remoteAddress = '127.0.0.1'; |
||||||
|
|
||||||
|
public function send($data) { |
||||||
|
$this->last[__FUNCTION__] = $data; |
||||||
|
} |
||||||
|
|
||||||
|
public function close() { |
||||||
|
$this->last[__FUNCTION__] = true; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Mock; |
||||||
|
use Ratchet\AbstractConnectionDecorator; |
||||||
|
|
||||||
|
class ConnectionDecorator extends AbstractConnectionDecorator { |
||||||
|
public $last = array( |
||||||
|
'write' => '' |
||||||
|
, 'end' => false |
||||||
|
); |
||||||
|
|
||||||
|
public function send($data) { |
||||||
|
$this->last[__FUNCTION__] = $data; |
||||||
|
|
||||||
|
$this->getConnection()->send($data); |
||||||
|
} |
||||||
|
|
||||||
|
public function close() { |
||||||
|
$this->last[__FUNCTION__] = true; |
||||||
|
|
||||||
|
$this->getConnection()->close(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Mock; |
||||||
|
use Ratchet\Wamp\WampServerInterface; |
||||||
|
use Ratchet\WebSocket\WsServerInterface; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
|
||||||
|
class WampComponent implements WampServerInterface, WsServerInterface { |
||||||
|
public $last = array(); |
||||||
|
|
||||||
|
public $protocols = array(); |
||||||
|
|
||||||
|
public function getSubProtocols() { |
||||||
|
return $this->protocols; |
||||||
|
} |
||||||
|
|
||||||
|
public function onCall(ConnectionInterface $conn, $id, $procURI, array $params) { |
||||||
|
$this->last[__FUNCTION__] = func_get_args(); |
||||||
|
} |
||||||
|
|
||||||
|
public function onSubscribe(ConnectionInterface $conn, $topic) { |
||||||
|
$this->last[__FUNCTION__] = func_get_args(); |
||||||
|
} |
||||||
|
|
||||||
|
public function onUnSubscribe(ConnectionInterface $conn, $topic) { |
||||||
|
$this->last[__FUNCTION__] = func_get_args(); |
||||||
|
} |
||||||
|
|
||||||
|
public function onPublish(ConnectionInterface $conn, $topic, $event, array $exclude, array $eligible) { |
||||||
|
$this->last[__FUNCTION__] = func_get_args(); |
||||||
|
} |
||||||
|
|
||||||
|
public function onOpen(ConnectionInterface $conn) { |
||||||
|
$this->last[__FUNCTION__] = func_get_args(); |
||||||
|
} |
||||||
|
|
||||||
|
public function onClose(ConnectionInterface $conn) { |
||||||
|
$this->last[__FUNCTION__] = func_get_args(); |
||||||
|
} |
||||||
|
|
||||||
|
public function onError(ConnectionInterface $conn, \Exception $e) { |
||||||
|
$this->last[__FUNCTION__] = func_get_args(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,28 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet; |
||||||
|
use Ratchet\ConnectionInterface; |
||||||
|
use Ratchet\MessageComponentInterface; |
||||||
|
use Ratchet\WebSocket\WsServerInterface; |
||||||
|
use Ratchet\Wamp\WampServerInterface; |
||||||
|
|
||||||
|
class NullComponent implements MessageComponentInterface, WsServerInterface, WampServerInterface { |
||||||
|
public function onOpen(ConnectionInterface $conn) {} |
||||||
|
|
||||||
|
public function onMessage(ConnectionInterface $conn, $msg) {} |
||||||
|
|
||||||
|
public function onClose(ConnectionInterface $conn) {} |
||||||
|
|
||||||
|
public function onError(ConnectionInterface $conn, \Exception $e) {} |
||||||
|
|
||||||
|
public function onCall(ConnectionInterface $conn, $id, $topic, array $params) {} |
||||||
|
|
||||||
|
public function onSubscribe(ConnectionInterface $conn, $topic) {} |
||||||
|
|
||||||
|
public function onUnSubscribe(ConnectionInterface $conn, $topic) {} |
||||||
|
|
||||||
|
public function onPublish(ConnectionInterface $conn, $topic, $event, array $exclude = array(), array $eligible = array()) {} |
||||||
|
|
||||||
|
public function getSubProtocols() { |
||||||
|
return array(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Wamp\Stub; |
||||||
|
use Ratchet\WebSocket\WsServerInterface; |
||||||
|
use Ratchet\Wamp\WampServerInterface; |
||||||
|
|
||||||
|
interface WsWampServerInterface extends WsServerInterface, WampServerInterface { |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\WebSocket\Stub; |
||||||
|
use Ratchet\MessageComponentInterface; |
||||||
|
use Ratchet\WebSocket\WsServerInterface; |
||||||
|
|
||||||
|
interface WsMessageComponentInterface extends MessageComponentInterface, WsServerInterface { |
||||||
|
} |
@ -0,0 +1,53 @@ |
|||||||
|
<?php |
||||||
|
use Guzzle\Http\Message\Request; |
||||||
|
|
||||||
|
class GuzzleTest extends \PHPUnit_Framework_TestCase { |
||||||
|
protected $_request; |
||||||
|
|
||||||
|
protected $_headers = array( |
||||||
|
'Upgrade' => 'websocket' |
||||||
|
, 'Connection' => 'Upgrade' |
||||||
|
, 'Host' => 'localhost:8080' |
||||||
|
, 'Origin' => 'chrome://newtab' |
||||||
|
, 'Sec-WebSocket-Protocol' => 'one, two, three' |
||||||
|
, 'Sec-WebSocket-Key' => '9bnXNp3ae6FbFFRtPdiPXA==' |
||||||
|
, 'Sec-WebSocket-Version' => '13' |
||||||
|
); |
||||||
|
|
||||||
|
public function setUp() { |
||||||
|
$this->_request = new Request('GET', 'http://localhost', $this->_headers); |
||||||
|
} |
||||||
|
|
||||||
|
public function testGetHeaderString() { |
||||||
|
$this->assertEquals('Upgrade', (string)$this->_request->getHeader('connection')); |
||||||
|
$this->assertEquals('9bnXNp3ae6FbFFRtPdiPXA==', (string)$this->_request->getHeader('Sec-Websocket-Key')); |
||||||
|
} |
||||||
|
|
||||||
|
public function testGetHeaderInteger() { |
||||||
|
$this->assertSame('13', (string)$this->_request->getHeader('Sec-Websocket-Version')); |
||||||
|
$this->assertSame(13, (int)(string)$this->_request->getHeader('Sec-WebSocket-Version')); |
||||||
|
} |
||||||
|
|
||||||
|
public function testGetHeaderObject() { |
||||||
|
$this->assertInstanceOf('Guzzle\Http\Message\Header', $this->_request->getHeader('Origin')); |
||||||
|
$this->assertNull($this->_request->getHeader('Non-existant-header')); |
||||||
|
} |
||||||
|
|
||||||
|
public function testHeaderObjectNormalizeValues() { |
||||||
|
$expected = 1 + substr_count($this->_headers['Sec-WebSocket-Protocol'], ','); |
||||||
|
$protocols = $this->_request->getHeader('Sec-WebSocket-Protocol')->normalize(); |
||||||
|
$count = 0; |
||||||
|
|
||||||
|
foreach ($protocols as $protocol) { |
||||||
|
$count++; |
||||||
|
} |
||||||
|
|
||||||
|
$this->assertEquals($expected, $count); |
||||||
|
$this->assertEquals($expected, count($protocols)); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRequestFactoryCreateSignature() { |
||||||
|
$ref = new \ReflectionMethod('Guzzle\Http\Message\RequestFactory', 'create'); |
||||||
|
$this->assertEquals(2, $ref->getNumberOfRequiredParameters()); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,147 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet; |
||||||
|
use Ratchet\Mock\ConnectionDecorator; |
||||||
|
|
||||||
|
/** |
||||||
|
* @covers Ratchet\AbstractConnectionDecorator |
||||||
|
* @covers Ratchet\ConnectionInterface |
||||||
|
*/ |
||||||
|
class AbstractConnectionDecoratorTest extends \PHPUnit_Framework_TestCase { |
||||||
|
protected $mock; |
||||||
|
protected $l1; |
||||||
|
protected $l2; |
||||||
|
|
||||||
|
public function setUp() { |
||||||
|
$this->mock = $this->getMock('\Ratchet\ConnectionInterface'); |
||||||
|
$this->l1 = new ConnectionDecorator($this->mock); |
||||||
|
$this->l2 = new ConnectionDecorator($this->l1); |
||||||
|
} |
||||||
|
|
||||||
|
public function testGet() { |
||||||
|
$var = 'hello'; |
||||||
|
$val = 'world'; |
||||||
|
|
||||||
|
$this->mock->$var = $val; |
||||||
|
|
||||||
|
$this->assertEquals($val, $this->l1->$var); |
||||||
|
$this->assertEquals($val, $this->l2->$var); |
||||||
|
} |
||||||
|
|
||||||
|
public function testSet() { |
||||||
|
$var = 'Chris'; |
||||||
|
$val = 'Boden'; |
||||||
|
|
||||||
|
$this->l1->$var = $val; |
||||||
|
|
||||||
|
$this->assertEquals($val, $this->mock->$var); |
||||||
|
} |
||||||
|
|
||||||
|
public function testSetLevel2() { |
||||||
|
$var = 'Try'; |
||||||
|
$val = 'Again'; |
||||||
|
|
||||||
|
$this->l2->$var = $val; |
||||||
|
|
||||||
|
$this->assertEquals($val, $this->mock->$var); |
||||||
|
} |
||||||
|
|
||||||
|
public function testIsSetTrue() { |
||||||
|
$var = 'PHP'; |
||||||
|
$val = 'Ratchet'; |
||||||
|
|
||||||
|
$this->mock->$var = $val; |
||||||
|
|
||||||
|
$this->assertTrue(isset($this->l1->$var)); |
||||||
|
$this->assertTrue(isset($this->l2->$var)); |
||||||
|
} |
||||||
|
|
||||||
|
public function testIsSetFalse() { |
||||||
|
$var = 'herp'; |
||||||
|
$val = 'derp'; |
||||||
|
|
||||||
|
$this->assertFalse(isset($this->l1->$var)); |
||||||
|
$this->assertFalse(isset($this->l2->$var)); |
||||||
|
} |
||||||
|
|
||||||
|
public function testUnset() { |
||||||
|
$var = 'Flying'; |
||||||
|
$val = 'Monkey'; |
||||||
|
|
||||||
|
$this->mock->$var = $val; |
||||||
|
unset($this->l1->$var); |
||||||
|
|
||||||
|
$this->assertFalse(isset($this->mock->$var)); |
||||||
|
} |
||||||
|
|
||||||
|
public function testUnsetLevel2() { |
||||||
|
$var = 'Flying'; |
||||||
|
$val = 'Monkey'; |
||||||
|
|
||||||
|
$this->mock->$var = $val; |
||||||
|
unset($this->l2->$var); |
||||||
|
|
||||||
|
$this->assertFalse(isset($this->mock->$var)); |
||||||
|
} |
||||||
|
|
||||||
|
public function testGetConnection() { |
||||||
|
$class = new \ReflectionClass('\\Ratchet\\AbstractConnectionDecorator'); |
||||||
|
$method = $class->getMethod('getConnection'); |
||||||
|
$method->setAccessible(true); |
||||||
|
|
||||||
|
$conn = $method->invokeArgs($this->l1, array()); |
||||||
|
|
||||||
|
$this->assertSame($this->mock, $conn); |
||||||
|
} |
||||||
|
|
||||||
|
public function testGetConnectionLevel2() { |
||||||
|
$class = new \ReflectionClass('\\Ratchet\\AbstractConnectionDecorator'); |
||||||
|
$method = $class->getMethod('getConnection'); |
||||||
|
$method->setAccessible(true); |
||||||
|
|
||||||
|
$conn = $method->invokeArgs($this->l2, array()); |
||||||
|
|
||||||
|
$this->assertSame($this->l1, $conn); |
||||||
|
} |
||||||
|
|
||||||
|
public function testWrapperCanStoreSelfInDecorator() { |
||||||
|
$this->mock->decorator = $this->l1; |
||||||
|
|
||||||
|
$this->assertSame($this->l1, $this->l2->decorator); |
||||||
|
} |
||||||
|
|
||||||
|
public function testDecoratorRecursion() { |
||||||
|
$this->mock->decorator = new \stdClass; |
||||||
|
$this->mock->decorator->conn = $this->l1; |
||||||
|
|
||||||
|
$this->assertSame($this->l1, $this->mock->decorator->conn); |
||||||
|
$this->assertSame($this->l1, $this->l1->decorator->conn); |
||||||
|
$this->assertSame($this->l1, $this->l2->decorator->conn); |
||||||
|
} |
||||||
|
|
||||||
|
public function testDecoratorRecursionLevel2() { |
||||||
|
$this->mock->decorator = new \stdClass; |
||||||
|
$this->mock->decorator->conn = $this->l2; |
||||||
|
|
||||||
|
$this->assertSame($this->l2, $this->mock->decorator->conn); |
||||||
|
$this->assertSame($this->l2, $this->l1->decorator->conn); |
||||||
|
$this->assertSame($this->l2, $this->l2->decorator->conn); |
||||||
|
|
||||||
|
// just for fun |
||||||
|
$this->assertSame($this->l2, $this->l2->decorator->conn->decorator->conn->decorator->conn); |
||||||
|
} |
||||||
|
|
||||||
|
public function testWarningGettingNothing() { |
||||||
|
$this->setExpectedException('PHPUnit_Framework_Error'); |
||||||
|
$var = $this->mock->nonExistant; |
||||||
|
} |
||||||
|
|
||||||
|
public function testWarningGettingNothingLevel1() { |
||||||
|
$this->setExpectedException('PHPUnit_Framework_Error'); |
||||||
|
$var = $this->l1->nonExistant; |
||||||
|
} |
||||||
|
|
||||||
|
public function testWarningGettingNothingLevel2() { |
||||||
|
$this->setExpectedException('PHPUnit_Framework_Error'); |
||||||
|
$var = $this->l2->nonExistant; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,67 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Http\Guzzle\Http\Message; |
||||||
|
use Ratchet\Http\Guzzle\Http\Message\RequestFactory; |
||||||
|
|
||||||
|
/** |
||||||
|
* @covers Ratchet\Http\Guzzle\Http\Message\RequestFactory |
||||||
|
*/ |
||||||
|
class RequestFactoryTest extends \PHPUnit_Framework_TestCase { |
||||||
|
protected $factory; |
||||||
|
|
||||||
|
public function setUp() { |
||||||
|
$this->factory = RequestFactory::getInstance(); |
||||||
|
} |
||||||
|
|
||||||
|
public function testMessageProvider() { |
||||||
|
return array( |
||||||
|
'status' => 'GET / HTTP/1.1' |
||||||
|
, 'headers' => array( |
||||||
|
'Upgrade' => 'WebSocket' |
||||||
|
, 'Connection' => 'Upgrade' |
||||||
|
, 'Host' => 'localhost:8000' |
||||||
|
, 'Sec-WebSocket-Key1' => '> b3lU Z0 fh f 3+83394 6 (zG4' |
||||||
|
, 'Sec-WebSocket-Key2' => ',3Z0X0677 dV-d [159 Z*4' |
||||||
|
) |
||||||
|
, 'body' => "123456\r\n\r\n" |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
public function combineMessage($status, array $headers, $body = '') { |
||||||
|
$message = $status . "\r\n"; |
||||||
|
|
||||||
|
foreach ($headers as $key => $val) { |
||||||
|
$message .= "{$key}: {$val}\r\n"; |
||||||
|
} |
||||||
|
|
||||||
|
$message .= "\r\n{$body}"; |
||||||
|
|
||||||
|
return $message; |
||||||
|
} |
||||||
|
|
||||||
|
public function testExpectedDataFromGuzzleHeaders() { |
||||||
|
$parts = $this->testMessageProvider(); |
||||||
|
$message = $this->combineMessage($parts['status'], $parts['headers'], $parts['body']); |
||||||
|
$object = $this->factory->fromMessage($message); |
||||||
|
|
||||||
|
foreach ($parts['headers'] as $key => $val) { |
||||||
|
$this->assertEquals($val, $object->getHeader($key, true)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public function testExpectedDataFromNonGuzzleHeaders() { |
||||||
|
$parts = $this->testMessageProvider(); |
||||||
|
$message = $this->combineMessage($parts['status'], $parts['headers'], $parts['body']); |
||||||
|
$object = $this->factory->fromMessage($message); |
||||||
|
|
||||||
|
$this->assertNull($object->getHeader('Nope', true)); |
||||||
|
$this->assertNull($object->getHeader('Nope')); |
||||||
|
} |
||||||
|
|
||||||
|
public function testExpectedDataFromNonGuzzleBody() { |
||||||
|
$parts = $this->testMessageProvider(); |
||||||
|
$message = $this->combineMessage($parts['status'], $parts['headers'], $parts['body']); |
||||||
|
$object = $this->factory->fromMessage($message); |
||||||
|
|
||||||
|
$this->assertEquals($parts['body'], (string)$object->getBody()); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,51 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Http; |
||||||
|
use Ratchet\Http\HttpRequestParser; |
||||||
|
|
||||||
|
/** |
||||||
|
* @covers Ratchet\Http\HttpRequestParser |
||||||
|
*/ |
||||||
|
class HttpRequestParserTest extends \PHPUnit_Framework_TestCase { |
||||||
|
protected $parser; |
||||||
|
|
||||||
|
public function setUp() { |
||||||
|
$this->parser = new HttpRequestParser; |
||||||
|
} |
||||||
|
|
||||||
|
public function headersProvider() { |
||||||
|
return array( |
||||||
|
array(false, "GET / HTTP/1.1\r\nHost: socketo.me\r\n") |
||||||
|
, array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n") |
||||||
|
, array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n1") |
||||||
|
, array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie✖") |
||||||
|
, array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie✖\r\n\r\n") |
||||||
|
, array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie\r\n") |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dataProvider headersProvider |
||||||
|
*/ |
||||||
|
public function testIsEom($expected, $message) { |
||||||
|
$this->assertEquals($expected, $this->parser->isEom($message)); |
||||||
|
} |
||||||
|
|
||||||
|
public function testBufferOverflowResponse() { |
||||||
|
$conn = $this->getMock('\Ratchet\ConnectionInterface'); |
||||||
|
|
||||||
|
$this->parser->maxSize = 20; |
||||||
|
|
||||||
|
$this->assertNull($this->parser->onMessage($conn, "GET / HTTP/1.1\r\n")); |
||||||
|
|
||||||
|
$this->setExpectedException('OverflowException'); |
||||||
|
|
||||||
|
$this->parser->onMessage($conn, "Header-Is: Too Big"); |
||||||
|
} |
||||||
|
|
||||||
|
public function testReturnTypeIsRequest() { |
||||||
|
$conn = $this->getMock('\Ratchet\ConnectionInterface'); |
||||||
|
$return = $this->parser->onMessage($conn, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n"); |
||||||
|
|
||||||
|
$this->assertInstanceOf('\Guzzle\Http\Message\RequestInterface', $return); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,64 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Http; |
||||||
|
use Ratchet\AbstractMessageComponentTestCase; |
||||||
|
|
||||||
|
/** |
||||||
|
* @covers Ratchet\Http\HttpServer |
||||||
|
*/ |
||||||
|
class HttpServerTest extends AbstractMessageComponentTestCase { |
||||||
|
public function setUp() { |
||||||
|
parent::setUp(); |
||||||
|
$this->_conn->httpHeadersReceived = true; |
||||||
|
} |
||||||
|
|
||||||
|
public function getConnectionClassString() { |
||||||
|
return '\Ratchet\ConnectionInterface'; |
||||||
|
} |
||||||
|
|
||||||
|
public function getDecoratorClassString() { |
||||||
|
return '\Ratchet\Http\HttpServer'; |
||||||
|
} |
||||||
|
|
||||||
|
public function getComponentClassString() { |
||||||
|
return '\Ratchet\Http\HttpServerInterface'; |
||||||
|
} |
||||||
|
|
||||||
|
public function testOpen() { |
||||||
|
$headers = "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n"; |
||||||
|
|
||||||
|
$this->_conn->httpHeadersReceived = false; |
||||||
|
$this->_app->expects($this->once())->method('onOpen')->with($this->isExpectedConnection()); |
||||||
|
$this->_serv->onMessage($this->_conn, $headers); |
||||||
|
} |
||||||
|
|
||||||
|
public function testOnMessageAfterHeaders() { |
||||||
|
$headers = "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n"; |
||||||
|
$this->_conn->httpHeadersReceived = false; |
||||||
|
$this->_serv->onMessage($this->_conn, $headers); |
||||||
|
|
||||||
|
$message = "Hello World!"; |
||||||
|
$this->_app->expects($this->once())->method('onMessage')->with($this->isExpectedConnection(), $message); |
||||||
|
$this->_serv->onMessage($this->_conn, $message); |
||||||
|
} |
||||||
|
|
||||||
|
public function testBufferOverflow() { |
||||||
|
$this->_conn->expects($this->once())->method('close'); |
||||||
|
$this->_conn->httpHeadersReceived = false; |
||||||
|
|
||||||
|
$this->_serv->onMessage($this->_conn, str_repeat('a', 5000)); |
||||||
|
} |
||||||
|
|
||||||
|
public function testCloseIfNotEstablished() { |
||||||
|
$this->_conn->httpHeadersReceived = false; |
||||||
|
$this->_conn->expects($this->once())->method('close'); |
||||||
|
$this->_serv->onError($this->_conn, new \Exception('Whoops!')); |
||||||
|
} |
||||||
|
|
||||||
|
public function testBufferHeaders() { |
||||||
|
$this->_conn->httpHeadersReceived = false; |
||||||
|
$this->_app->expects($this->never())->method('onOpen'); |
||||||
|
$this->_app->expects($this->never())->method('onMessage'); |
||||||
|
|
||||||
|
$this->_serv->onMessage($this->_conn, "GET / HTTP/1.1"); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,46 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Http; |
||||||
|
use Ratchet\AbstractMessageComponentTestCase; |
||||||
|
|
||||||
|
/** |
||||||
|
* @covers Ratchet\Http\OriginCheck |
||||||
|
*/ |
||||||
|
class OriginCheckTest extends AbstractMessageComponentTestCase { |
||||||
|
protected $_reqStub; |
||||||
|
|
||||||
|
public function setUp() { |
||||||
|
$this->_reqStub = $this->getMock('Guzzle\Http\Message\RequestInterface'); |
||||||
|
$this->_reqStub->expects($this->any())->method('getHeader')->will($this->returnValue('localhost')); |
||||||
|
|
||||||
|
parent::setUp(); |
||||||
|
|
||||||
|
$this->_serv->allowedOrigins[] = 'localhost'; |
||||||
|
} |
||||||
|
|
||||||
|
protected function doOpen($conn) { |
||||||
|
$this->_serv->onOpen($conn, $this->_reqStub); |
||||||
|
} |
||||||
|
|
||||||
|
public function getConnectionClassString() { |
||||||
|
return '\Ratchet\ConnectionInterface'; |
||||||
|
} |
||||||
|
|
||||||
|
public function getDecoratorClassString() { |
||||||
|
return '\Ratchet\Http\OriginCheck'; |
||||||
|
} |
||||||
|
|
||||||
|
public function getComponentClassString() { |
||||||
|
return '\Ratchet\Http\HttpServerInterface'; |
||||||
|
} |
||||||
|
|
||||||
|
public function testCloseOnNonMatchingOrigin() { |
||||||
|
$this->_serv->allowedOrigins = array('socketo.me'); |
||||||
|
$this->_conn->expects($this->once())->method('close'); |
||||||
|
|
||||||
|
$this->_serv->onOpen($this->_conn, $this->_reqStub); |
||||||
|
} |
||||||
|
|
||||||
|
public function testOnMessage() { |
||||||
|
$this->passthroughMessageTest('Hello World!'); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,140 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Http; |
||||||
|
use Ratchet\Http\Router; |
||||||
|
use Ratchet\WebSocket\WsServerInterface; |
||||||
|
use Symfony\Component\Routing\Exception\ResourceNotFoundException; |
||||||
|
use Symfony\Component\Routing\Matcher\UrlMatcherInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* @covers Ratchet\Http\Router |
||||||
|
*/ |
||||||
|
class RouterTest extends \PHPUnit_Framework_TestCase { |
||||||
|
protected $_router; |
||||||
|
protected $_matcher; |
||||||
|
protected $_conn; |
||||||
|
protected $_req; |
||||||
|
|
||||||
|
public function setUp() { |
||||||
|
$queryMock = $this->getMock('Guzzle\Http\QueryString'); |
||||||
|
$queryMock |
||||||
|
->expects($this->any()) |
||||||
|
->method('getAll') |
||||||
|
->will($this->returnValue(array())); |
||||||
|
|
||||||
|
$this->_conn = $this->getMock('\Ratchet\ConnectionInterface'); |
||||||
|
$this->_req = $this->getMock('\Guzzle\Http\Message\RequestInterface'); |
||||||
|
$this->_req |
||||||
|
->expects($this->any()) |
||||||
|
->method('getQuery') |
||||||
|
->will($this->returnValue($queryMock)); |
||||||
|
$this->_matcher = $this->getMock('Symfony\Component\Routing\Matcher\UrlMatcherInterface'); |
||||||
|
$this->_matcher |
||||||
|
->expects($this->any()) |
||||||
|
->method('getContext') |
||||||
|
->will($this->returnValue($this->getMock('Symfony\Component\Routing\RequestContext'))); |
||||||
|
$this->_router = new Router($this->_matcher); |
||||||
|
|
||||||
|
$this->_req->expects($this->any())->method('getPath')->will($this->returnValue('/whatever')); |
||||||
|
} |
||||||
|
|
||||||
|
public function testFourOhFour() { |
||||||
|
$this->_conn->expects($this->once())->method('close'); |
||||||
|
|
||||||
|
$nope = new ResourceNotFoundException; |
||||||
|
$this->_matcher->expects($this->any())->method('match')->will($this->throwException($nope)); |
||||||
|
|
||||||
|
$this->_router->onOpen($this->_conn, $this->_req); |
||||||
|
} |
||||||
|
|
||||||
|
public function testNullRequest() { |
||||||
|
$this->setExpectedException('\UnexpectedValueException'); |
||||||
|
$this->_router->onOpen($this->_conn); |
||||||
|
} |
||||||
|
|
||||||
|
public function testControllerIsMessageComponentInterface() { |
||||||
|
$this->setExpectedException('\UnexpectedValueException'); |
||||||
|
$this->_matcher->expects($this->any())->method('match')->will($this->returnValue(array('_controller' => new \StdClass))); |
||||||
|
$this->_router->onOpen($this->_conn, $this->_req); |
||||||
|
} |
||||||
|
|
||||||
|
public function testControllerOnOpen() { |
||||||
|
$controller = $this->getMockBuilder('\Ratchet\WebSocket\WsServer')->disableOriginalConstructor()->getMock(); |
||||||
|
$this->_matcher->expects($this->any())->method('match')->will($this->returnValue(array('_controller' => $controller))); |
||||||
|
$this->_router->onOpen($this->_conn, $this->_req); |
||||||
|
|
||||||
|
$expectedConn = new \PHPUnit_Framework_Constraint_IsInstanceOf('\Ratchet\ConnectionInterface'); |
||||||
|
$controller->expects($this->once())->method('onOpen')->with($expectedConn, $this->_req); |
||||||
|
|
||||||
|
$this->_matcher->expects($this->any())->method('match')->will($this->returnValue(array('_controller' => $controller))); |
||||||
|
$this->_router->onOpen($this->_conn, $this->_req); |
||||||
|
} |
||||||
|
|
||||||
|
public function testControllerOnMessageBubbles() { |
||||||
|
$message = "The greatest trick the Devil ever pulled was convincing the world he didn't exist"; |
||||||
|
$controller = $this->getMockBuilder('\Ratchet\WebSocket\WsServer')->disableOriginalConstructor()->getMock(); |
||||||
|
$controller->expects($this->once())->method('onMessage')->with($this->_conn, $message); |
||||||
|
|
||||||
|
$this->_conn->controller = $controller; |
||||||
|
|
||||||
|
$this->_router->onMessage($this->_conn, $message); |
||||||
|
} |
||||||
|
|
||||||
|
public function testControllerOnCloseBubbles() { |
||||||
|
$controller = $this->getMockBuilder('\Ratchet\WebSocket\WsServer')->disableOriginalConstructor()->getMock(); |
||||||
|
$controller->expects($this->once())->method('onClose')->with($this->_conn); |
||||||
|
|
||||||
|
$this->_conn->controller = $controller; |
||||||
|
|
||||||
|
$this->_router->onClose($this->_conn); |
||||||
|
} |
||||||
|
|
||||||
|
public function testControllerOnErrorBubbles() { |
||||||
|
$e= new \Exception('One cannot be betrayed if one has no exceptions'); |
||||||
|
$controller = $this->getMockBuilder('\Ratchet\WebSocket\WsServer')->disableOriginalConstructor()->getMock(); |
||||||
|
$controller->expects($this->once())->method('onError')->with($this->_conn, $e); |
||||||
|
|
||||||
|
$this->_conn->controller = $controller; |
||||||
|
|
||||||
|
$this->_router->onError($this->_conn, $e); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRouterGeneratesRouteParameters() { |
||||||
|
/** @var $controller WsServerInterface */ |
||||||
|
$controller = $this->getMockBuilder('\Ratchet\WebSocket\WsServer')->disableOriginalConstructor()->getMock(); |
||||||
|
/** @var $matcher UrlMatcherInterface */ |
||||||
|
$this->_matcher->expects($this->any())->method('match')->will( |
||||||
|
$this->returnValue(array('_controller' => $controller, 'foo' => 'bar', 'baz' => 'qux')) |
||||||
|
); |
||||||
|
$conn = $this->getMock('Ratchet\Mock\Connection'); |
||||||
|
|
||||||
|
$request = $this->getMock('Guzzle\Http\Message\Request', array('getPath'), array('GET', 'ws://random.url'), '', false); |
||||||
|
$request->expects($this->any())->method('getPath')->will($this->returnValue('ws://doesnt.matter/')); |
||||||
|
|
||||||
|
$request->setHeaderFactory($this->getMock('Guzzle\Http\Message\Header\HeaderFactoryInterface')); |
||||||
|
$request->setUrl('ws://doesnt.matter/'); |
||||||
|
|
||||||
|
$router = new Router($this->_matcher); |
||||||
|
|
||||||
|
$router->onOpen($conn, $request); |
||||||
|
|
||||||
|
$this->assertEquals(array('foo' => 'bar', 'baz' => 'qux'), $request->getQuery()->getAll()); |
||||||
|
} |
||||||
|
|
||||||
|
public function testQueryParams() { |
||||||
|
$controller = $this->getMockBuilder('\Ratchet\WebSocket\WsServer')->disableOriginalConstructor()->getMock(); |
||||||
|
$this->_matcher->expects($this->any())->method('match')->will( |
||||||
|
$this->returnValue(array('_controller' => $controller, 'foo' => 'bar', 'baz' => 'qux')) |
||||||
|
); |
||||||
|
|
||||||
|
$conn = $this->getMock('Ratchet\Mock\Connection'); |
||||||
|
$request = $this->getMock('Guzzle\Http\Message\Request', array('getPath'), array('GET', ''), '', false); |
||||||
|
|
||||||
|
$request->setHeaderFactory($this->getMock('Guzzle\Http\Message\Header\HeaderFactoryInterface')); |
||||||
|
$request->setUrl('ws://doesnt.matter?hello=world&foo=nope'); |
||||||
|
|
||||||
|
$router = new Router($this->_matcher); |
||||||
|
$router->onOpen($conn, $request); |
||||||
|
|
||||||
|
$this->assertEquals(array('foo' => 'nope', 'baz' => 'qux', 'hello' => 'world'), $request->getQuery()->getAll()); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Server; |
||||||
|
use Ratchet\Server\EchoServer; |
||||||
|
|
||||||
|
class EchoServerTest extends \PHPUnit_Framework_TestCase { |
||||||
|
protected $_conn; |
||||||
|
protected $_comp; |
||||||
|
|
||||||
|
public function setUp() { |
||||||
|
$this->_conn = $this->getMock('\Ratchet\ConnectionInterface'); |
||||||
|
$this->_comp = new EchoServer; |
||||||
|
} |
||||||
|
|
||||||
|
public function testMessageEchod() { |
||||||
|
$message = 'Tillsonburg, my back still aches when I hear that word.'; |
||||||
|
$this->_conn->expects($this->once())->method('send')->with($message); |
||||||
|
$this->_comp->onMessage($this->_conn, $message); |
||||||
|
} |
||||||
|
|
||||||
|
public function testErrorClosesConnection() { |
||||||
|
ob_start(); |
||||||
|
$this->_conn->expects($this->once())->method('close'); |
||||||
|
$this->_comp->onError($this->_conn, new \Exception); |
||||||
|
ob_end_clean(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,152 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Application\Server; |
||||||
|
use Ratchet\Server\FlashPolicy; |
||||||
|
|
||||||
|
/** |
||||||
|
* @covers Ratchet\Server\FlashPolicy |
||||||
|
*/ |
||||||
|
class FlashPolicyTest extends \PHPUnit_Framework_TestCase { |
||||||
|
|
||||||
|
protected $_policy; |
||||||
|
|
||||||
|
public function setUp() { |
||||||
|
$this->_policy = new FlashPolicy(); |
||||||
|
} |
||||||
|
|
||||||
|
public function testPolicyRender() { |
||||||
|
$this->_policy->setSiteControl('all'); |
||||||
|
$this->_policy->addAllowedAccess('example.com', '*'); |
||||||
|
$this->_policy->addAllowedAccess('dev.example.com', '*'); |
||||||
|
|
||||||
|
$this->assertInstanceOf('SimpleXMLElement', $this->_policy->renderPolicy()); |
||||||
|
} |
||||||
|
|
||||||
|
public function testInvalidPolicyReader() { |
||||||
|
$this->setExpectedException('UnexpectedValueException'); |
||||||
|
$this->_policy->renderPolicy(); |
||||||
|
} |
||||||
|
|
||||||
|
public function testInvalidDomainPolicyReader() { |
||||||
|
$this->setExpectedException('UnexpectedValueException'); |
||||||
|
$this->_policy->setSiteControl('all'); |
||||||
|
$this->_policy->addAllowedAccess('dev.example.*', '*'); |
||||||
|
$this->_policy->renderPolicy(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dataProvider siteControl |
||||||
|
*/ |
||||||
|
public function testSiteControlValidation($accept, $permittedCrossDomainPolicies) { |
||||||
|
$this->assertEquals($accept, $this->_policy->validateSiteControl($permittedCrossDomainPolicies)); |
||||||
|
} |
||||||
|
|
||||||
|
public static function siteControl() { |
||||||
|
return array( |
||||||
|
array(true, 'all') |
||||||
|
, array(true, 'none') |
||||||
|
, array(true, 'master-only') |
||||||
|
, array(false, 'by-content-type') |
||||||
|
, array(false, 'by-ftp-filename') |
||||||
|
, array(false, '') |
||||||
|
, array(false, 'all ') |
||||||
|
, array(false, 'asdf') |
||||||
|
, array(false, '@893830') |
||||||
|
, array(false, '*') |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dataProvider URI |
||||||
|
*/ |
||||||
|
public function testDomainValidation($accept, $domain) { |
||||||
|
$this->assertEquals($accept, $this->_policy->validateDomain($domain)); |
||||||
|
} |
||||||
|
|
||||||
|
public static function URI() { |
||||||
|
return array( |
||||||
|
array(true, '*') |
||||||
|
, array(true, 'example.com') |
||||||
|
, array(true, 'exam-ple.com') |
||||||
|
, array(true, '*.example.com') |
||||||
|
, array(true, 'www.example.com') |
||||||
|
, array(true, 'dev.dev.example.com') |
||||||
|
, array(true, 'http://example.com') |
||||||
|
, array(true, 'https://example.com') |
||||||
|
, array(true, 'http://*.example.com') |
||||||
|
, array(false, 'exam*ple.com') |
||||||
|
, array(true, '127.0.255.1') |
||||||
|
, array(true, 'localhost') |
||||||
|
, array(false, 'www.example.*') |
||||||
|
, array(false, 'www.exa*le.com') |
||||||
|
, array(false, 'www.example.*com') |
||||||
|
, array(false, '*.example.*') |
||||||
|
, array(false, 'gasldf*$#a0sdf0a8sdf') |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dataProvider ports |
||||||
|
*/ |
||||||
|
public function testPortValidation($accept, $ports) { |
||||||
|
$this->assertEquals($accept, $this->_policy->validatePorts($ports)); |
||||||
|
} |
||||||
|
|
||||||
|
public static function ports() { |
||||||
|
return array( |
||||||
|
array(true, '*') |
||||||
|
, array(true, '80') |
||||||
|
, array(true, '80,443') |
||||||
|
, array(true, '507,516-523') |
||||||
|
, array(true, '507,516-523,333') |
||||||
|
, array(true, '507,516-523,507,516-523') |
||||||
|
, array(false, '516-') |
||||||
|
, array(true, '516-523,11') |
||||||
|
, array(false, '516,-523,11') |
||||||
|
, array(false, 'example') |
||||||
|
, array(false, 'asdf,123') |
||||||
|
, array(false, '--') |
||||||
|
, array(false, ',,,') |
||||||
|
, array(false, '838*') |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
public function testAddAllowedAccessOnlyAcceptsValidPorts() { |
||||||
|
$this->setExpectedException('UnexpectedValueException'); |
||||||
|
|
||||||
|
$this->_policy->addAllowedAccess('*', 'nope'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testSetSiteControlThrowsException() { |
||||||
|
$this->setExpectedException('UnexpectedValueException'); |
||||||
|
|
||||||
|
$this->_policy->setSiteControl('nope'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testErrorClosesConnection() { |
||||||
|
$conn = $this->getMock('\\Ratchet\\ConnectionInterface'); |
||||||
|
$conn->expects($this->once())->method('close'); |
||||||
|
|
||||||
|
$this->_policy->onError($conn, new \Exception); |
||||||
|
} |
||||||
|
|
||||||
|
public function testOnMessageSendsString() { |
||||||
|
$this->_policy->addAllowedAccess('*', '*'); |
||||||
|
|
||||||
|
$conn = $this->getMock('\\Ratchet\\ConnectionInterface'); |
||||||
|
$conn->expects($this->once())->method('send')->with($this->isType('string')); |
||||||
|
|
||||||
|
$this->_policy->onMessage($conn, ' '); |
||||||
|
} |
||||||
|
|
||||||
|
public function testOnOpenExists() { |
||||||
|
$this->assertTrue(method_exists($this->_policy, 'onOpen')); |
||||||
|
$conn = $this->getMock('\Ratchet\ConnectionInterface'); |
||||||
|
$this->_policy->onOpen($conn); |
||||||
|
} |
||||||
|
|
||||||
|
public function testOnCloseExists() { |
||||||
|
$this->assertTrue(method_exists($this->_policy, 'onClose')); |
||||||
|
$conn = $this->getMock('\Ratchet\ConnectionInterface'); |
||||||
|
$this->_policy->onClose($conn); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Application\Server; |
||||||
|
use Ratchet\Server\IoConnection; |
||||||
|
|
||||||
|
/** |
||||||
|
* @covers Ratchet\Server\IoConnection |
||||||
|
*/ |
||||||
|
class IoConnectionTest extends \PHPUnit_Framework_TestCase { |
||||||
|
protected $sock; |
||||||
|
protected $conn; |
||||||
|
|
||||||
|
public function setUp() { |
||||||
|
$this->sock = $this->getMock('\\React\\Socket\\ConnectionInterface'); |
||||||
|
$this->conn = new IoConnection($this->sock); |
||||||
|
} |
||||||
|
|
||||||
|
public function testCloseBubbles() { |
||||||
|
$this->sock->expects($this->once())->method('end'); |
||||||
|
$this->conn->close(); |
||||||
|
} |
||||||
|
|
||||||
|
public function testSendBubbles() { |
||||||
|
$msg = '6 hour rides are productive'; |
||||||
|
|
||||||
|
$this->sock->expects($this->once())->method('write')->with($msg); |
||||||
|
$this->conn->send($msg); |
||||||
|
} |
||||||
|
|
||||||
|
public function testSendReturnsSelf() { |
||||||
|
$this->assertSame($this->conn, $this->conn->send('fluent interface')); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,118 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Server; |
||||||
|
use Ratchet\Server\IoServer; |
||||||
|
use React\EventLoop\StreamSelectLoop; |
||||||
|
use React\Socket\Server; |
||||||
|
|
||||||
|
/** |
||||||
|
* @covers Ratchet\Server\IoServer |
||||||
|
*/ |
||||||
|
class IoServerTest extends \PHPUnit_Framework_TestCase { |
||||||
|
protected $server; |
||||||
|
|
||||||
|
protected $app; |
||||||
|
|
||||||
|
protected $port; |
||||||
|
|
||||||
|
protected $reactor; |
||||||
|
|
||||||
|
public function setUp() { |
||||||
|
$this->app = $this->getMock('\\Ratchet\\MessageComponentInterface'); |
||||||
|
|
||||||
|
$loop = new StreamSelectLoop; |
||||||
|
$this->reactor = new Server($loop); |
||||||
|
$this->reactor->listen(0); |
||||||
|
|
||||||
|
$this->port = $this->reactor->getPort(); |
||||||
|
$this->server = new IoServer($this->app, $this->reactor, $loop); |
||||||
|
} |
||||||
|
|
||||||
|
public function testOnOpen() { |
||||||
|
$this->app->expects($this->once())->method('onOpen')->with($this->isInstanceOf('\\Ratchet\\ConnectionInterface')); |
||||||
|
|
||||||
|
$client = stream_socket_client("tcp://localhost:{$this->port}"); |
||||||
|
|
||||||
|
$this->server->loop->tick(); |
||||||
|
|
||||||
|
//$this->assertTrue(is_string($this->app->last['onOpen'][0]->remoteAddress)); |
||||||
|
//$this->assertTrue(is_int($this->app->last['onOpen'][0]->resourceId)); |
||||||
|
} |
||||||
|
|
||||||
|
public function testOnData() { |
||||||
|
$msg = 'Hello World!'; |
||||||
|
|
||||||
|
$this->app->expects($this->once())->method('onMessage')->with( |
||||||
|
$this->isInstanceOf('\\Ratchet\\ConnectionInterface') |
||||||
|
, $msg |
||||||
|
); |
||||||
|
|
||||||
|
$client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); |
||||||
|
socket_set_option($client, SOL_SOCKET, SO_REUSEADDR, 1); |
||||||
|
socket_set_option($client, SOL_SOCKET, SO_SNDBUF, 4096); |
||||||
|
socket_set_block($client); |
||||||
|
socket_connect($client, 'localhost', $this->port); |
||||||
|
|
||||||
|
$this->server->loop->tick(); |
||||||
|
|
||||||
|
socket_write($client, $msg); |
||||||
|
$this->server->loop->tick(); |
||||||
|
|
||||||
|
socket_shutdown($client, 1); |
||||||
|
socket_shutdown($client, 0); |
||||||
|
socket_close($client); |
||||||
|
|
||||||
|
$this->server->loop->tick(); |
||||||
|
} |
||||||
|
|
||||||
|
public function testOnClose() { |
||||||
|
$this->app->expects($this->once())->method('onClose')->with($this->isInstanceOf('\\Ratchet\\ConnectionInterface')); |
||||||
|
|
||||||
|
$client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); |
||||||
|
socket_set_option($client, SOL_SOCKET, SO_REUSEADDR, 1); |
||||||
|
socket_set_option($client, SOL_SOCKET, SO_SNDBUF, 4096); |
||||||
|
socket_set_block($client); |
||||||
|
socket_connect($client, 'localhost', $this->port); |
||||||
|
|
||||||
|
$this->server->loop->tick(); |
||||||
|
|
||||||
|
socket_shutdown($client, 1); |
||||||
|
socket_shutdown($client, 0); |
||||||
|
socket_close($client); |
||||||
|
|
||||||
|
$this->server->loop->tick(); |
||||||
|
} |
||||||
|
|
||||||
|
public function testFactory() { |
||||||
|
$this->assertInstanceOf('\\Ratchet\\Server\\IoServer', IoServer::factory($this->app, 0)); |
||||||
|
} |
||||||
|
|
||||||
|
public function testNoLoopProvidedError() { |
||||||
|
$this->setExpectedException('RuntimeException'); |
||||||
|
|
||||||
|
$io = new IoServer($this->app, $this->reactor); |
||||||
|
$io->run(); |
||||||
|
} |
||||||
|
|
||||||
|
public function testOnErrorPassesException() { |
||||||
|
$conn = $this->getMock('\\React\\Socket\\ConnectionInterface'); |
||||||
|
$conn->decor = $this->getMock('\\Ratchet\\ConnectionInterface'); |
||||||
|
$err = new \Exception("Nope"); |
||||||
|
|
||||||
|
$this->app->expects($this->once())->method('onError')->with($conn->decor, $err); |
||||||
|
|
||||||
|
$this->server->handleError($err, $conn); |
||||||
|
} |
||||||
|
|
||||||
|
public function onErrorCalledWhenExceptionThrown() { |
||||||
|
$this->markTestIncomplete("Need to learn how to throw an exception from a mock"); |
||||||
|
|
||||||
|
$conn = $this->getMock('\\React\\Socket\\ConnectionInterface'); |
||||||
|
$this->server->handleConnect($conn); |
||||||
|
|
||||||
|
$e = new \Exception; |
||||||
|
$this->app->expects($this->once())->method('onMessage')->with($this->isInstanceOf('\\Ratchet\\ConnectionInterface'), 'f')->will($e); |
||||||
|
$this->app->expects($this->once())->method('onError')->with($this->instanceOf('\\Ratchet\\ConnectionInterface', $e)); |
||||||
|
|
||||||
|
$this->server->handleData('f', $conn); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,125 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Server; |
||||||
|
use Ratchet\Server\IpBlackList; |
||||||
|
|
||||||
|
/** |
||||||
|
* @covers Ratchet\Server\IpBlackList |
||||||
|
*/ |
||||||
|
class IpBlackListTest extends \PHPUnit_Framework_TestCase { |
||||||
|
protected $blocker; |
||||||
|
protected $mock; |
||||||
|
|
||||||
|
public function setUp() { |
||||||
|
$this->mock = $this->getMock('\\Ratchet\\MessageComponentInterface'); |
||||||
|
$this->blocker = new IpBlackList($this->mock); |
||||||
|
} |
||||||
|
|
||||||
|
public function testOnOpen() { |
||||||
|
$this->mock->expects($this->exactly(3))->method('onOpen'); |
||||||
|
|
||||||
|
$conn1 = $this->newConn(); |
||||||
|
$conn2 = $this->newConn(); |
||||||
|
$conn3 = $this->newConn(); |
||||||
|
|
||||||
|
$this->blocker->onOpen($conn1); |
||||||
|
$this->blocker->onOpen($conn3); |
||||||
|
$this->blocker->onOpen($conn2); |
||||||
|
} |
||||||
|
|
||||||
|
public function testBlockDoesNotTriggerOnOpen() { |
||||||
|
$conn = $this->newConn(); |
||||||
|
|
||||||
|
$this->blocker->blockAddress($conn->remoteAddress); |
||||||
|
|
||||||
|
$this->mock->expects($this->never())->method('onOpen'); |
||||||
|
|
||||||
|
$ret = $this->blocker->onOpen($conn); |
||||||
|
} |
||||||
|
|
||||||
|
public function testBlockDoesNotTriggerOnClose() { |
||||||
|
$conn = $this->newConn(); |
||||||
|
|
||||||
|
$this->blocker->blockAddress($conn->remoteAddress); |
||||||
|
|
||||||
|
$this->mock->expects($this->never())->method('onClose'); |
||||||
|
|
||||||
|
$ret = $this->blocker->onOpen($conn); |
||||||
|
} |
||||||
|
|
||||||
|
public function testOnMessageDecoration() { |
||||||
|
$conn = $this->newConn(); |
||||||
|
$msg = 'Hello not being blocked'; |
||||||
|
|
||||||
|
$this->mock->expects($this->once())->method('onMessage')->with($conn, $msg); |
||||||
|
|
||||||
|
$this->blocker->onMessage($conn, $msg); |
||||||
|
} |
||||||
|
|
||||||
|
public function testOnCloseDecoration() { |
||||||
|
$conn = $this->newConn(); |
||||||
|
|
||||||
|
$this->mock->expects($this->once())->method('onClose')->with($conn); |
||||||
|
|
||||||
|
$this->blocker->onClose($conn); |
||||||
|
} |
||||||
|
|
||||||
|
public function testBlockClosesConnection() { |
||||||
|
$conn = $this->newConn(); |
||||||
|
$this->blocker->blockAddress($conn->remoteAddress); |
||||||
|
|
||||||
|
$conn->expects($this->once())->method('close'); |
||||||
|
|
||||||
|
$this->blocker->onOpen($conn); |
||||||
|
} |
||||||
|
|
||||||
|
public function testAddAndRemoveWithFluentInterfaces() { |
||||||
|
$blockOne = '127.0.0.1'; |
||||||
|
$blockTwo = '192.168.1.1'; |
||||||
|
$unblock = '75.119.207.140'; |
||||||
|
|
||||||
|
$this->blocker |
||||||
|
->blockAddress($unblock) |
||||||
|
->blockAddress($blockOne) |
||||||
|
->unblockAddress($unblock) |
||||||
|
->blockAddress($blockTwo) |
||||||
|
; |
||||||
|
|
||||||
|
$this->assertEquals(array($blockOne, $blockTwo), $this->blocker->getBlockedAddresses()); |
||||||
|
} |
||||||
|
|
||||||
|
public function testDecoratorPassesErrors() { |
||||||
|
$conn = $this->newConn(); |
||||||
|
$e = new \Exception('I threw an error'); |
||||||
|
|
||||||
|
$this->mock->expects($this->once())->method('onError')->with($conn, $e); |
||||||
|
|
||||||
|
$this->blocker->onError($conn, $e); |
||||||
|
} |
||||||
|
|
||||||
|
public function addressProvider() { |
||||||
|
return array( |
||||||
|
array('127.0.0.1', '127.0.0.1') |
||||||
|
, array('localhost', 'localhost') |
||||||
|
, array('fe80::1%lo0', 'fe80::1%lo0') |
||||||
|
, array('127.0.0.1', '127.0.0.1:6392') |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dataProvider addressProvider |
||||||
|
*/ |
||||||
|
public function testFilterAddress($expected, $input) { |
||||||
|
$this->assertEquals($expected, $this->blocker->filterAddress($input)); |
||||||
|
} |
||||||
|
|
||||||
|
public function testUnblockingSilentlyFails() { |
||||||
|
$this->assertInstanceOf('\\Ratchet\\Server\\IpBlackList', $this->blocker->unblockAddress('localhost')); |
||||||
|
} |
||||||
|
|
||||||
|
protected function newConn() { |
||||||
|
$conn = $this->getMock('\\Ratchet\\ConnectionInterface'); |
||||||
|
$conn->remoteAddress = '127.0.0.1'; |
||||||
|
|
||||||
|
return $conn; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
<?php |
||||||
|
namespace Ratchet\Session\Serialize; |
||||||
|
use Ratchet\Session\Serialize\PhpHandler; |
||||||
|
|
||||||
|
/** |
||||||
|
* @covers Ratchet\Session\Serialize\PhpHandler |
||||||
|
*/ |
||||||
|
class PhpHandlerTest extends \PHPUnit_Framework_TestCase { |
||||||
|
protected $_handler; |
||||||
|
|
||||||
|
public function setUp() { |
||||||
|
$this->_handler = new PhpHandler; |
||||||
|
} |
||||||
|
|
||||||
|
public function serializedProvider() { |
||||||
|
return array( |
||||||
|
array( |
||||||
|
'_sf2_attributes|a:2:{s:5:"hello";s:5:"world";s:4:"last";i:1332872102;}_sf2_flashes|a:0:{}' |
||||||
|
, array( |
||||||
|
'_sf2_attributes' => array( |
||||||
|
'hello' => 'world' |
||||||
|
, 'last' => 1332872102 |
||||||
|
) |
||||||
|
, '_sf2_flashes' => array() |
||||||
|
) |
||||||
|
) |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dataProvider serializedProvider |
||||||
|
*/ |
||||||
|
public function testUnserialize($in, $expected) { |
||||||
|
$this->assertEquals($expected, $this->_handler->unserialize($in)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @dataProvider serializedProvider |
||||||
|
*/ |
||||||
|
public function testSerialize($serialized, $original) { |
||||||
|
$this->assertEquals($serialized, $this->_handler->serialize($original)); |
||||||
|
} |
||||||
|
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue