Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
typo3
qfq
Commits
28b461fe
Commit
28b461fe
authored
Aug 30, 2020
by
Carsten Rose
Browse files
Refs #11076. First implementation of websocket (without wss/ssl)
parent
f5013b3b
Pipeline
#3732
passed with stages
in 5 minutes and 28 seconds
Changes
4
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
extension/Classes/Core/Constants.php
View file @
28b461fe
...
...
@@ -1603,6 +1603,7 @@ const COLUMN_IMG = "img";
const
COLUMN_MAILTO
=
"mailto"
;
const
COLUMN_SENDMAIL
=
"sendmail"
;
const
COLUMN_VERTICAL
=
"vertical"
;
const
COLUMN_WEBSOCKET
=
"websocket"
;
const
COLUMN_NO_WRAP
=
"noWrap"
;
const
COLUMN_HIDE
=
"hide"
;
...
...
@@ -1758,6 +1759,7 @@ const TOKEN_UID = 'uid';
const
TOKEN_DOWNLOAD
=
'd'
;
const
TOKEN_COPY_TO_CLIPBOARD
=
'y'
;
const
TOKEN_DROPDOWN
=
'z'
;
const
TOKEN_WEBSOCKET
=
'w'
;
const
TOKEN_TEXT
=
't'
;
const
TOKEN_ALT_TEXT
=
'a'
;
...
...
extension/Classes/Core/Report/Link.php
View file @
28b461fe
...
...
@@ -23,14 +23,14 @@
namespace
IMATHUZH\Qfq\Core\Report
;
use
IMATHUZH\Qfq\Core\Helper\KeyValueStringParser
;
use
IMATHUZH\Qfq\Core\Helper\OnArray
;
use
IMATHUZH\Qfq\Core\Helper\Sanitize
;
use
IMATHUZH\Qfq\Core\Helper\Support
;
use
IMATHUZH\Qfq\Core\Helper\Token
;
use
IMATHUZH\Qfq\Core\Helper\Sanitize
;
use
IMATHUZH\Qfq\Core\Store\Sip
;
use
IMATHUZH\Qfq\Core\Store\Store
;
use
IMATHUZH\Qfq\Core\Report\WebSocket
;
/*
* a:AltText
...
...
@@ -77,7 +77,7 @@ use IMATHUZH\Qfq\Core\Store\Store;
* U:URL Param
* v:
* V:
* w:
* w:
websocket
* W:Dimension
* x:Delete
* X:
...
...
@@ -519,13 +519,56 @@ class Link {
return
$this
->
renderLink
(
KeyValueStringParser
::
unparse
(
$paramArr
,
PARAM_TOKEN_DELIMITER
,
PARAM_DELIMITER
));
}
/**
* @param $str
* @return string
* @throws \UserFormException
* @throws \UserReportException
*/
public
function
processWebSocket
(
$str
)
{
$websocket
=
new
WebSocket
();
$answer
=
''
;
// str="w:wss://antmedia.math.uzh.ch:6334/test|t:<payload>|timeout:..."
$param
=
KeyValueStringParser
::
parse
(
$str
,
PARAM_TOKEN_DELIMITER
,
PARAM_DELIMITER
);
if
(
empty
(
$param
[
TOKEN_WEBSOCKET
])
||
empty
(
$param
[
TOKEN_TEXT
]))
{
throw
new
\
UserReportException
(
"Missing Websocket target or text to send"
,
ERROR_MISSING_VALUE
);
}
$urlParts
=
parse_url
(
$param
[
TOKEN_WEBSOCKET
]);
if
(
empty
(
$urlParts
[
'host'
])
||
empty
(
$urlParts
[
'port'
])
||
empty
(
$urlParts
[
'path'
]))
{
throw
new
\
UserFormException
(
json_encode
([
ERROR_MESSAGE_TO_USER
=>
'Target URL incomplete'
,
ERROR_MESSAGE_TO_DEVELOPER
=>
'host:'
.
$urlParts
[
'host'
]
.
', '
.
'port:'
.
$urlParts
[
'port'
]
.
', '
.
'path:'
.
$urlParts
[
'path'
]])
,
ERROR_MISSING_VALUE
);
}
// Open Socket
if
(
false
===
$websocket
->
connect
(
$urlParts
[
'host'
],
$urlParts
[
'port'
],
$urlParts
[
'path'
]))
{
throw
new
\
UserFormException
(
json_encode
([
ERROR_MESSAGE_TO_USER
=>
'Failed connect websocket'
,
ERROR_MESSAGE_TO_DEVELOPER
=>
'host:'
.
$urlParts
[
'host'
]
.
', '
.
'port:'
.
$urlParts
[
'port'
]
.
', '
.
'path:'
.
$urlParts
[
'path'
]])
,
ERROR_MISSING_VALUE
);
}
$answer
=
$websocket
->
sendData
(
$param
[
TOKEN_TEXT
]);
return
$answer
;
}
/**
* Build the whole link.
*
* @param string $str Qualifier with params. 'report'-syntax. F.e.: u:www.example.com|P:home.gif|t:Home"
*
* @return string The complete HTML encoded Link like
* <a href='http://example.com' class='external'><img src='icon
f
.gif' title='help text'>Description</a>
* <a href='http://example.com' class='external'><img src='icon.gif' title='help text'>Description</a>
* @throws \CodeException
* @throws \UserFormException
* @throws \UserReportException
...
...
@@ -539,7 +582,18 @@ class Link {
return
''
;
}
// Check for dropdown menu
switch
(
$str
[
0
]
??
''
)
{
case
TOKEN_DROPDOWN
:
// Check for dropdown menu
return
$this
->
processDropdown
(
$str
);
break
;
case
TOKEN_WEBSOCKET
:
return
$this
->
processWebSocket
(
$str
);
break
;
default
:
break
;
}
if
((
$str
[
0
]
??
''
)
==
TOKEN_DROPDOWN
)
{
return
$this
->
processDropdown
(
$str
);
}
...
...
@@ -673,7 +727,6 @@ class Link {
$flagArray
=
array
();
// str="u:http://www.example.com|c:i|t:Hello World|q:Do you really want to delete the record 25:warn:yes:no"
// $param = explode(PARAM_DELIMITER, $str);
$param
=
KeyValueStringParser
::
explodeEscape
(
PARAM_DELIMITER
,
$str
);
$param
=
$this
->
paramPriority
(
$param
);
...
...
@@ -807,7 +860,6 @@ class Link {
NAME_DELETE
=>
''
,
NAME_MONITOR
=>
'0'
,
NAME_COPY_TO_CLIPBOARD
=>
''
,
NAME_LINK_CLASS
=>
''
,
// class name
NAME_LINK_CLASS_DEFAULT
=>
''
,
// Depending of 'as page' or 'as url'. Only used if class is not explicit set.
...
...
extension/Classes/Core/Report/Report.php
View file @
28b461fe
...
...
@@ -675,7 +675,7 @@ class Report {
/**
* Called with an array of column names.
* Each column name can be split in multiple string by '|': [s1[|s2[|s3]]]
* Each s1|s2|s3 can be: {title}, _{special colum name}, _hide, _noWrap, _={title}, _+{tag}, _<{tag1}><{tag2}>
* Each s1|s2|s3 can be: {title}, _{special colum
n
name}, _hide, _noWrap, _={title}, _+{tag}, _<{tag1}><{tag2}>
*
* Return an Array: newKeys[idx][C_FULL|C_TITLE|C_NO_WRAP|C_HIDE]
*
...
...
@@ -906,7 +906,7 @@ class Report {
* @param string $columnValue
* @param string $full_level
* @param string $rowIndex
* @param $flagOutput
* @param
bool
$flagOutput
*
* @return string rendered column
* @throws \CodeException
...
...
@@ -938,6 +938,7 @@ class Report {
switch
(
$columnName
)
{
case
COLUMN_LINK
:
case
COLUMN_WEBSOCKET
:
$content
.
=
$this
->
link
->
renderLink
(
$columnValue
);
break
;
...
...
extension/Classes/Core/Report/WebSocket.php
0 → 100644
View file @
28b461fe
<?php
namespace
IMATHUZH\Qfq\Core\Report
;
/**
* Simple WebSocket client.
*
* Class WebSocket
* @package qfq
*
* @author Simon Samtleben <foo@bloatless.org>
* @version 2.0
*/
class
WebSocket
{
/**
* @var string $host
*/
private
$host
;
/**
* @var int $port
*/
private
$port
;
/**
* @var string $path
*/
private
$path
;
/**
* @var string $origin
*/
private
$origin
;
/**
* @var resource $socket
*/
private
$socket
=
null
;
/**
* @var bool $connected
*/
private
$connected
=
false
;
/**
* @var string $target
*/
private
$target
=
''
;
public
function
__destruct
()
{
$this
->
disconnect
();
}
/**
* Sends data to remote server.
*
* @param string $data
* @param string $type
* @param bool $masked
* @return bool
*/
public
function
sendData
(
string
$data
,
string
$type
=
'text'
,
bool
$masked
=
true
)
{
if
(
$this
->
connected
===
false
)
{
trigger_error
(
"Not connected"
,
E_USER_WARNING
);
return
false
;
}
if
(
!
is_string
(
$data
))
{
trigger_error
(
"Not a string data was given."
,
E_USER_WARNING
);
return
false
;
}
if
(
strlen
(
$data
)
===
0
)
{
return
false
;
}
$res
=
@
fwrite
(
$this
->
socket
,
$this
->
hybi10Encode
(
$data
,
$type
,
$masked
));
if
(
$res
===
0
||
$res
===
false
)
{
return
false
;
}
$buffer
=
' '
;
$answer
=
''
;
while
(
$buffer
!==
''
)
{
$buffer
=
fread
(
$this
->
socket
,
512
);
// drop?
$answer
.
=
$buffer
;
}
// It seems we have two control characters at the beginning: don't understand why.
if
(
ord
(
$answer
[
0
])
==
129
)
{
$answer
=
substr
(
$answer
,
2
);
}
return
$answer
;
}
/**
* @param string $data
* @param string $type
* @param bool $masked
* @return mixed|string
*/
public
function
sendAndWait
(
string
$data
,
string
$type
=
'text'
,
bool
$masked
=
true
)
{
if
(
$this
->
sendData
(
$data
,
$type
,
$masked
))
{
// to be implemented - for now it waits a bit and sends a predefined message
usleep
(
250
);
$msg
=
json_decode
(
$data
,
true
);
$msg
[
'type'
]
=
'response'
;
$msg
[
'token'
]
=
'some_random_unusable_token'
;
return
json_decode
(
$msg
);
}
else
{
// error!
return
''
;
}
/* $this->connected = false;
// send ping:
$data = 'ping?';
@fwrite($this->socket, $this->hybi10Encode($data, 'ping', true));
$response = @fread($this->socket, 300);
if (empty($response)) {
return false;
}
$response = $this->hybi10Decode($response);
if (!is_array($response)) {
return false;
}
if (!isset($response['type']) || $response['type'] !== 'pong') {
return false;
}
$this->connected = true;
return true;*/
}
/**
* Connects to a websocket server.
*
* @param string $host
* @param int $port
* @param string $path
* @param string $origin
* @return bool
*/
public
function
connect
(
string
$host
,
int
$port
,
string
$path
,
string
$origin
=
''
)
{
$this
->
host
=
$host
;
$this
->
port
=
$port
;
$this
->
path
=
$path
;
$this
->
origin
=
$origin
;
$key
=
base64_encode
(
$this
->
generateRandomString
(
16
,
false
,
true
));
$header
=
"GET "
.
$path
.
" HTTP/1.1
\r\n
"
;
$header
.
=
"Host: "
.
$host
.
":"
.
$port
.
"
\r\n
"
;
$header
.
=
"Upgrade: websocket
\r\n
"
;
$header
.
=
"Connection: Upgrade
\r\n
"
;
$header
.
=
"Sec-WebSocket-Key: "
.
$key
.
"
\r\n
"
;
if
(
!
empty
(
$origin
))
{
$header
.
=
"Sec-WebSocket-Origin: "
.
$origin
.
"
\r\n
"
;
}
$header
.
=
"Sec-WebSocket-Version: 13
\r\n\r\n
"
;
$this
->
socket
=
fsockopen
(
$host
,
$port
,
$errno
,
$errstr
,
2
);
if
(
$this
->
socket
===
false
)
{
return
false
;
}
socket_set_timeout
(
$this
->
socket
,
0
,
10000
);
@
fwrite
(
$this
->
socket
,
$header
);
$response
=
@
fread
(
$this
->
socket
,
1500
);
preg_match
(
'#Sec-WebSocket-Accept:\s(.*)$#mU'
,
$response
,
$matches
);
if
(
$matches
)
{
$keyAccept
=
trim
(
$matches
[
1
]);
$expectedResponse
=
base64_encode
(
pack
(
'H*'
,
sha1
(
$key
.
'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
)));
$this
->
connected
=
(
$keyAccept
===
$expectedResponse
)
?
true
:
false
;
}
return
$this
->
connected
;
}
/**
* Checks if connection to webserver is active.
*
* @return bool
*/
public
function
checkConnection
()
{
$this
->
connected
=
false
;
// send ping:
$data
=
'ping?'
;
@
fwrite
(
$this
->
socket
,
$this
->
hybi10Encode
(
$data
,
'ping'
,
true
));
$response
=
@
fread
(
$this
->
socket
,
300
);
if
(
empty
(
$response
))
{
return
false
;
}
$response
=
$this
->
hybi10Decode
(
$response
);
if
(
!
is_array
(
$response
))
{
return
false
;
}
if
(
!
isset
(
$response
[
'type'
])
||
$response
[
'type'
]
!==
'pong'
)
{
return
false
;
}
$this
->
connected
=
true
;
return
true
;
}
/**
* Disconnects from websocket server.
*
* @return void
*/
public
function
disconnect
()
{
$this
->
connected
=
false
;
is_resource
(
$this
->
socket
)
&&
fclose
(
$this
->
socket
);
}
/**
* Reconnects to previously connected websocket server.
*
* @return void
*/
public
function
reconnect
()
{
sleep
(
10
);
$this
->
connected
=
false
;
fclose
(
$this
->
socket
);
$this
->
connect
(
$this
->
host
,
$this
->
port
,
$this
->
path
,
$this
->
origin
);
}
/**
* Generates a random string.
*
* @param int $length
* @param bool $addSpaces
* @param bool $addNumbers
* @return string
*/
private
function
generateRandomString
(
int
$length
=
10
,
bool
$addSpaces
=
true
,
bool
$addNumbers
=
true
)
{
$characters
=
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"§$%&/()=[]{}'
;
$useChars
=
[];
// select some random chars:
for
(
$i
=
0
;
$i
<
$length
;
$i
++
)
{
$useChars
[]
=
$characters
[
mt_rand
(
0
,
strlen
(
$characters
)
-
1
)];
}
// add spaces and numbers:
if
(
$addSpaces
===
true
)
{
array_push
(
$useChars
,
' '
,
' '
,
' '
,
' '
,
' '
,
' '
);
}
if
(
$addNumbers
===
true
)
{
array_push
(
$useChars
,
rand
(
0
,
9
),
rand
(
0
,
9
),
rand
(
0
,
9
));
}
shuffle
(
$useChars
);
$randomString
=
trim
(
implode
(
''
,
$useChars
));
$randomString
=
substr
(
$randomString
,
0
,
$length
);
return
$randomString
;
}
/**
* Encodes data according to the WebSocket protocol standard.
*
* @param string $payload
* @param string $type
* @param bool $masked
* @return string
*/
private
function
hybi10Encode
(
string
$payload
,
string
$type
=
'text'
,
bool
$masked
=
true
)
{
$frameHead
=
[];
$payloadLength
=
strlen
(
$payload
);
switch
(
$type
)
{
case
'text'
:
// first byte indicates FIN, Text-Frame (10000001):
$frameHead
[
0
]
=
129
;
break
;
case
'close'
:
// first byte indicates FIN, Close Frame(10001000):
$frameHead
[
0
]
=
136
;
break
;
case
'ping'
:
// first byte indicates FIN, Ping frame (10001001):
$frameHead
[
0
]
=
137
;
break
;
case
'pong'
:
// first byte indicates FIN, Pong frame (10001010):
$frameHead
[
0
]
=
138
;
break
;
}
// set mask and payload length (using 1, 3 or 9 bytes)
if
(
$payloadLength
>
65535
)
{
$payloadLengthBin
=
str_split
(
sprintf
(
'%064b'
,
$payloadLength
),
8
);
$frameHead
[
1
]
=
(
$masked
===
true
)
?
255
:
127
;
for
(
$i
=
0
;
$i
<
8
;
$i
++
)
{
$frameHead
[
$i
+
2
]
=
bindec
(
$payloadLengthBin
[
$i
]);
}
// most significant bit MUST be 0 (close connection if frame too big)
if
(
$frameHead
[
2
]
>
127
)
{
$this
->
disconnect
();
throw
new
\
RuntimeException
(
'Invalid payload. Could not encode frame.'
);
}
}
elseif
(
$payloadLength
>
125
)
{
$payloadLengthBin
=
str_split
(
sprintf
(
'%016b'
,
$payloadLength
),
8
);
$frameHead
[
1
]
=
(
$masked
===
true
)
?
254
:
126
;
$frameHead
[
2
]
=
bindec
(
$payloadLengthBin
[
0
]);
$frameHead
[
3
]
=
bindec
(
$payloadLengthBin
[
1
]);
}
else
{
$frameHead
[
1
]
=
(
$masked
===
true
)
?
$payloadLength
+
128
:
$payloadLength
;
}
// convert frame-head to string:
foreach
(
array_keys
(
$frameHead
)
as
$i
)
{
$frameHead
[
$i
]
=
chr
(
$frameHead
[
$i
]);
}
if
(
$masked
===
true
)
{
// generate a random mask:
$mask
=
[];
for
(
$i
=
0
;
$i
<
4
;
$i
++
)
{
$mask
[
$i
]
=
chr
(
rand
(
0
,
255
));
}
$frameHead
=
array_merge
(
$frameHead
,
$mask
);
}
$frame
=
implode
(
''
,
$frameHead
);
// append payload to frame:
for
(
$i
=
0
;
$i
<
$payloadLength
;
$i
++
)
{
$frame
.
=
(
$masked
===
true
)
?
$payload
[
$i
]
^
$mask
[
$i
%
4
]
:
$payload
[
$i
];
}
return
$frame
;
}
/**
* Decodes a received frame/string according to the WebSocket protocol standards.
*
* @param string $data
* @return array
*/
private
function
hybi10Decode
(
string
$data
)
{
$unmaskedPayload
=
''
;
$decodedData
=
[];
// estimate frame type:
$firstByteBinary
=
sprintf
(
'%08b'
,
ord
(
$data
[
0
]));
$secondByteBinary
=
sprintf
(
'%08b'
,
ord
(
$data
[
1
]));
$opcode
=
bindec
(
substr
(
$firstByteBinary
,
4
,
4
));
$isMasked
=
(
$secondByteBinary
[
0
]
==
'1'
)
?
true
:
false
;
$payloadLength
=
ord
(
$data
[
1
])
&
127
;
switch
(
$opcode
)
{
// text frame:
case
1
:
$decodedData
[
'type'
]
=
'text'
;
break
;
case
2
:
$decodedData
[
'type'
]
=
'binary'
;
break
;
// connection close frame:
case
8
:
$decodedData
[
'type'
]
=
'close'
;
break
;
// ping frame:
case
9
:
$decodedData
[
'type'
]
=
'ping'
;
break
;
// pong frame:
case
10
:
$decodedData
[
'type'
]
=
'pong'
;
break
;
default
:
throw
new
\
RuntimeException
(
'Could not decode frame. Invalid type.'
);
}
if
(
$payloadLength
===
126
)
{
$mask
=
substr
(
$data
,
4
,
4
);
$payloadOffset
=
8
;
$dataLength
=
bindec
(
sprintf
(
'%08b'
,
ord
(
$data
[
2
]))
.
sprintf
(
'%08b'
,
ord
(
$data
[
3
])))
+
$payloadOffset
;
}
elseif
(
$payloadLength
===
127
)
{
$mask
=
substr
(
$data
,
10
,
4
);
$payloadOffset
=
14
;
$tmp
=
''
;
for
(
$i
=
0
;
$i
<
8
;
$i
++
)
{
$tmp
.
=
sprintf
(
'%08b'
,
ord
(
$data
[
$i
+
2
]));
}
$dataLength
=
bindec
(
$tmp
)
+
$payloadOffset
;
unset
(
$tmp
);
}
else
{
$mask
=
substr
(
$data
,
2
,
4
);
$payloadOffset
=
6
;
$dataLength
=
$payloadLength
+
$payloadOffset
;
}
if
(
$isMasked
===
true
)
{
for
(
$i
=
$payloadOffset
;
$i
<
$dataLength
;
$i
++
)
{
$j
=
$i
-
$payloadOffset
;
if
(
isset
(
$data
[
$i
]))
{
$unmaskedPayload
.
=
$data
[
$i
]
^
$mask
[
$j
%
4
];
}
}
$decodedData
[
'payload'
]
=
$unmaskedPayload
;
}
else
{
$payloadOffset
=
$payloadOffset
-
4
;
$decodedData
[
'payload'
]
=
substr
(
$data
,
$payloadOffset
);
}
return
$decodedData
;
}
}
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment