| 1 | <?php date_default_timezone_set("Europe/Warsaw"); |
| 2 | define("EXIT_AFTERWARDS", true);
|
| 3 |
|
| 4 | if ( "cli" != php_sapi_name() )
|
| 5 | die("This tool is intended to be used from CLI");
|
| 6 |
|
| 7 | |
| 8 | |
| 9 | |
| 10 |
|
| 11 | # TODO: DTLS ;)
|
| 12 |
|
| 13 | register_shutdown_function(function(){
|
| 14 | |
| 15 | |
| 16 | global $socket;
|
| 17 | if ( is_object($socket) ){
|
| 18 | $err = socket_last_error($socket);
|
| 19 | error("{$err} - ".socket_strerror($err)."\n");
|
| 20 | }
|
| 21 | });
|
| 22 |
|
| 23 | function error($msg){
|
| 24 | |
| 25 | static $errh = null; if ( $errh == null ) $errh = fopen("php://stderr", "w");
|
| 26 | fwrite($errh, $msg);
|
| 27 | }
|
| 28 |
|
| 29 | function msg($msg, $isJSON = false){
|
| 30 | |
| 31 | global $replyJSON; if ( $replyJSON && !$isJSON ) return;
|
| 32 | echo $msg;
|
| 33 | }
|
| 34 |
|
| 35 | function socket_reconnect(&$socket, string $addr, int $port){
|
| 36 | |
| 37 | if ( is_object($socket) ){
|
| 38 | socket_close($socket);
|
| 39 | $socket = null;
|
| 40 | }
|
| 41 |
|
| 42 | $socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
|
| 43 | if ( $socket === null )
|
| 44 | die("ERR: failed to (re)create the socket; issue: ");
|
| 45 |
|
| 46 | socket_connect($socket, $addr, $port);
|
| 47 | }
|
| 48 |
|
| 49 | function resortResultArray(array &$data){
|
| 50 | |
| 51 | $newArray = [];
|
| 52 |
|
| 53 | |
| 54 | foreach(['Success', 'Hostname', 'Address', 'ServerVersion', 'ServerState', 'InternalRouter', 'QueryPort', 'BeaconPort', 'GamePort'] as $value)
|
| 55 | if ( isset($data[$value]) )
|
| 56 | $newArray[$value] = $data[$value];
|
| 57 |
|
| 58 | |
| 59 | foreach($data as $name => $value)
|
| 60 | if ( !isset($newArray[$name]) )
|
| 61 | $newArray[$name] = $value;
|
| 62 |
|
| 63 | $data = $newArray;
|
| 64 | }
|
| 65 |
|
| 66 | function socket_test_port(socket &$socket, int $portNumber, string $portType){
|
| 67 | |
| 68 | |
| 69 | |
| 70 |
|
| 71 | |
| 72 | $portType = strtoupper($portType);
|
| 73 | if ( $portType != "BCON" && $portType != "GAME" ){
|
| 74 | socket_close($socket); $socket = null;
|
| 75 | die("ERR: socket_test_port(): unrecognised port type '{$portType}' passed (use only BCON or GAME).\n");
|
| 76 | }
|
| 77 |
|
| 78 | |
| 79 | msg("Testing {$portType} ({$portNumber}) port reachability..");
|
| 80 | $buffer = $portType . " ";
|
| 81 | $result = socket_send($socket, $buffer, strlen($buffer), 0);
|
| 82 | $timeout = TIMEOUT * 2;
|
| 83 | do {
|
| 84 | $reads = [$socket];
|
| 85 | $timeout--;
|
| 86 | if ( $timeout <= 0 ){
|
| 87 | msg(" TIMEOUT\n");
|
| 88 | error("FAILED to talk to the {$portType} ({$portNumber}) port in " . TIMEOUT . " seconds, aborting.\n");
|
| 89 | return false;
|
| 90 | }
|
| 91 | msg(".");
|
| 92 | } while ( socket_select($reads, $__ignore1, $__ignore2, 0, 500000) < 1 );
|
| 93 |
|
| 94 | |
| 95 | $timeout = TIMEOUT;
|
| 96 | while ( socket_select($reads, $__ignore1, $__ignore2, 0, 500000) >= 1 ) {
|
| 97 | $reads = [$socket];
|
| 98 | @socket_recv($socket, $readData, 1024, null);
|
| 99 | if ( $readData !== null && strlen($readData) >= 5 ){
|
| 100 | |
| 101 | break;
|
| 102 | }
|
| 103 | $timeout--;
|
| 104 | if ( $timeout <= 0 ){
|
| 105 | msg(" TIMEOUT\n");
|
| 106 | error("FAILED to read the reply in " . TIMEOUT . " seconds, aborting.\n");
|
| 107 | return false;
|
| 108 | }
|
| 109 | msg(".");
|
| 110 | }
|
| 111 |
|
| 112 | |
| 113 | |
| 114 | if ( substr($readData, 0, 4) != $portType){
|
| 115 | msg("ERR: unknown reply from the {$portType} ({$portNumber}) port, aborting.\n");
|
| 116 | return false;
|
| 117 | }
|
| 118 | msg(" OK\n");
|
| 119 | return true;
|
| 120 | }
|
| 121 |
|
| 122 | function reply_JSON($data, $kill = false){
|
| 123 | |
| 124 |
|
| 125 | $data['TimeoutSeconds'] = TIMEOUT;
|
| 126 | resortResultArray($data);
|
| 127 | msg(json_encode($data), true);
|
| 128 |
|
| 129 | if ( $kill == EXIT_AFTERWARDS ){
|
| 130 | global $socket;
|
| 131 | socket_close($socket);
|
| 132 | $socket = null;
|
| 133 | die(1);
|
| 134 | }
|
| 135 | }
|
| 136 |
|
| 137 | |
| 138 | $serverAddress = "127.0.0.1";
|
| 139 | $serverName = "localhost";
|
| 140 | $serverPort = "15777";
|
| 141 | $replyJSON = false;
|
| 142 |
|
| 143 | define("TIMEOUT", 10); |
| 144 | #####################################################
|
| 145 | |
| 146 | |
| 147 | #####################################################
|
| 148 |
|
| 149 | if ( isset($argv[1]) )
|
| 150 | $serverPort = $argv[1];
|
| 151 |
|
| 152 | if ( isset($argv[2]) )
|
| 153 | $serverAddress = $argv[2];
|
| 154 |
|
| 155 | if ( isset($argv[3]) && $argv[3] == 1 )
|
| 156 | $replyJSON = true;
|
| 157 |
|
| 158 | $serverData = ['Success' => false,
|
| 159 | 'QueryPort' => (int) $serverPort, 'BeaconPort' => 0, 'GamePort' => 0,
|
| 160 | 'ServerVersion' => 0, 'ServerState' => "Not tested",
|
| 161 | 'Address' => 0];
|
| 162 | if ( filter_var($serverAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 | FILTER_FLAG_IPV4) ){
|
| 163 | |
| 164 | $serverData['Hostname'] = "(unknown)";
|
| 165 | $serverData['Address'] = $serverAddress;
|
| 166 | } else if ( preg_match("/^[^\.]+\.[^\.]{2,}.*$/", $serverAddress) || $serverAddress == "localhost" ) {
|
| 167 | |
| 168 | $serverData['Hostname'] = $serverAddress;
|
| 169 | $serverAddress = gethostbyname($serverData['Hostname']);
|
| 170 | if ( $serverAddress == $serverData['Hostname'] ){
|
| 171 | $serverData['Address'] = "Hostname resolution failed";
|
| 172 | if ( $replyJSON ) reply_JSON($serverData, EXIT_AFTERWARDS);
|
| 173 | error("ERR: hostname '{$serverName}' failed to resolve\n");
|
| 174 | die();
|
| 175 | }
|
| 176 | $serverData['Address'] = $serverAddress;
|
| 177 | } else {
|
| 178 | |
| 179 | if ( $replyJSON ) reply_JSON($serverData, EXIT_AFTERWARDS);
|
| 180 | error("ERR: provided address '{$serverAddress}' not understood.\n");
|
| 181 | die();
|
| 182 | }
|
| 183 |
|
| 184 | if ( !$replyJSON ){
|
| 185 | msg("Using server address: " . $serverData['Address'] . ($serverData['Hostname'] != "(unknown)" ? " ({$serverName})" : "") ."\n");
|
| 186 | msg("Comm timeout is " . TIMEOUT ." seconds\n");
|
| 187 | }
|
| 188 |
|
| 189 | |
| 190 | socket_reconnect($socket, $serverData['Address'], $serverData['QueryPort']);
|
| 191 |
|
| 192 | |
| 193 | $result = @socket_send($socket, str_pad("", 10, chr(0)), 10, 0);
|
| 194 |
|
| 195 | |
| 196 | |
| 197 | if ( false === $result ){
|
| 198 | $serverData['QueryPort'] = 0;
|
| 199 | if ( $replyJSON ) reply_JSON($serverData, EXIT_AFTERWARDS);
|
| 200 | die("Failed to create socket: "); |
| 201 | }
|
| 202 |
|
| 203 | msg("Querying {$serverAddress}:{$serverPort}..");
|
| 204 | |
| 205 | $timeout = TIMEOUT * 2;
|
| 206 | do {
|
| 207 | $reads = [$socket];
|
| 208 | $timeout--;
|
| 209 | if ( $timeout <= 0 ){
|
| 210 | msg(" TIMEOUT\n");
|
| 211 | msg("FAILED to reach server in " . TIMEOUT . " seconds, aborting.\n");
|
| 212 | if ( $replyJSON ) reply_JSON($serverData, EXIT_AFTERWARDS);
|
| 213 | socket_close($socket); $socket = null; die();
|
| 214 | }
|
| 215 | msg(".");
|
| 216 | } while ( socket_select($reads, $__ignore1, $__ignore2, 0, 500000) < 1 );
|
| 217 |
|
| 218 | |
| 219 | $readLength = @socket_recv($socket, $readData, 1024, 0);
|
| 220 |
|
| 221 | |
| 222 | if ( $readLength === false ){
|
| 223 | if ( socket_last_error($socket) == 10054 ){
|
| 224 | $serverData['ServerState'] = "No reply";
|
| 225 | if ( $replyJSON ) reply_JSON($serverData, EXIT_AFTERWARDS);
|
| 226 | socket_close($socket); $socket = null;
|
| 227 | die(" ERR: connection failed; the server is likely not running.\n");
|
| 228 | }
|
| 229 | if ( $replyJSON ) reply_JSON($serverData, EXIT_AFTERWARDS);
|
| 230 | die(" ERR: failed to read from socket: "); |
| 231 | }
|
| 232 |
|
| 233 | msg(" OK\n");
|
| 234 |
|
| 235 | |
| 236 | if ( $readLength != 17 ){
|
| 237 | $serverData['ServerState'] = "Host up";
|
| 238 | $serverData['ProtocolVersion'] = "(unknown)";
|
| 239 | if ( $replyJSON ) reply_JSON($serverData, EXIT_AFTERWARDS);
|
| 240 | die(" ERR: failed to read exactly 17 bytes, read {$readLength} bytes instead\n"); |
| 241 | }
|
| 242 |
|
| 243 | $readData = unpack("CID/CProtocolVersion/QIgnore/CServerState/VServerVersion/vBeaconPort", $readData);
|
| 244 |
|
| 245 | |
| 246 | if ( $readData['ID'] != 1 ){
|
| 247 | $serverData['ServerState'] = "Host up";
|
| 248 | $serverData['ProtocolVersion'] = "(unknown)";
|
| 249 | if ( $replyJSON ) reply_JSON($serverData, EXIT_AFTERWARDS);
|
| 250 | msg(" ERR: reply ID != 1\n");
|
| 251 | socket_close($socket); $socket = null; die(); |
| 252 | }
|
| 253 |
|
| 254 | |
| 255 | if ( $readData['ProtocolVersion'] != 0 ){
|
| 256 | $serverData['ServerState'] = "Host up";
|
| 257 | $serverData['ProtocolVersion'] = "(unknown)";
|
| 258 | if ( $replyJSON ) reply_JSON($serverData, EXIT_AFTERWARDS);
|
| 259 | msg(" ERR: unknown protocol version {$readData['ProtocolVersion']} != 0\n");
|
| 260 | socket_close($socket); $socket = null; die(); |
| 261 | }
|
| 262 |
|
| 263 | |
| 264 | foreach(['ServerState', 'ServerVersion', 'BeaconPort'] as $valueName)
|
| 265 | $serverData[$valueName] = $readData[$valueName];
|
| 266 |
|
| 267 | |
| 268 | switch($serverData['ServerState']){
|
| 269 | case 1:
|
| 270 | $serverData['ServerState'] = "Idle";
|
| 271 | break;
|
| 272 |
|
| 273 | case 2:
|
| 274 | $serverData['ServerState'] = "Loading";
|
| 275 | break;
|
| 276 |
|
| 277 | case 3:
|
| 278 | $serverData['ServerState'] = "Playing";
|
| 279 | break;
|
| 280 |
|
| 281 | default:
|
| 282 | $serverData['ServerState'] = "Unrecognised ({$readData['ServerState']})";
|
| 283 | if ( $replyJSON ) reply_JSON($serverData, EXIT_AFTERWARDS);
|
| 284 | msg(" ERR: unrecognised server state\n");
|
| 285 | socket_close($socket); $socket = null; die(); |
| 286 | break;
|
| 287 | }
|
| 288 |
|
| 289 | if ( !$replyJSON ){
|
| 290 | msg("Server version: {$serverData['ServerVersion']}, state: {$serverData['ServerState']}\n");
|
| 291 | }
|
| 292 |
|
| 293 | $serverData['QueryPort'] = (int) $serverPort;
|
| 294 | $serverData['GamePort'] = 0;
|
| 295 | $serverData['InternalRouter'] = false;
|
| 296 | if ( $serverData['BeaconPort'] == $serverPort ){
|
| 297 | msg("Built-in router in use: YES.\n");
|
| 298 | $serverData['InternalRouter'] = true;
|
| 299 | $serverData['GamePort'] = (int) $serverPort;
|
| 300 | } else {
|
| 301 | msg("Built-in router in use: NO, separate port connections required.\n");
|
| 302 | }
|
| 303 |
|
| 304 |
|
| 305 | |
| 306 | socket_reconnect($socket, $serverAddress, $serverData['BeaconPort']);
|
| 307 | if ( !socket_test_port($socket, $serverData['BeaconPort'], "BCON") ){
|
| 308 | if ( $replyJSON ) reply_JSON($serverData, EXIT_AFTERWARDS);
|
| 309 | socket_close($socket); $socket = null; die();
|
| 310 | }
|
| 311 |
|
| 312 |
|
| 313 | |
| 314 | if ( $serverData['InternalRouter'] == false && $serverData['GamePort'] == 0 ){
|
| 315 | |
| 316 | |
| 317 | |
| 318 |
|
| 319 | |
| 320 | if ( $serverData['QueryPort'] == 15777 && $serverData['BeaconPort'] == 15000 ){
|
| 321 | |
| 322 | socket_reconnect($socket, $serverAddress, 7777);
|
| 323 | if ( socket_test_port($socket, 7777, "GAME") ){
|
| 324 | msg(" Be advised: port 7777 is a GUESS; it's possible that this is NOT the GAME port in use.\n");
|
| 325 | } else {
|
| 326 | msg(" It's possible that I guessed wrong?\n");
|
| 327 | }
|
| 328 | } else {
|
| 329 | msg("\n Built-in router not in use, no DTLS implementation available,\n and the setup does not follow default ports. Cannot test the\n GAME port as we don't know it and can't ask for it.\n");
|
| 330 | }
|
| 331 |
|
| 332 | |
| 333 | $serverData['Success'] = true;
|
| 334 | } else {
|
| 335 | |
| 336 | |
| 337 | |
| 338 | socket_reconnect($socket, $serverAddress, $serverData['GamePort']);
|
| 339 | if ( !socket_test_port($socket, $serverData['GamePort'], "GAME") ){
|
| 340 | if ( $replyJSON ) reply_JSON($serverData, EXIT_AFTERWARDS);
|
| 341 | socket_close($socket); $socket = null; die();
|
| 342 | }
|
| 343 |
|
| 344 | |
| 345 | $serverData['Success'] = true;
|
| 346 | }
|
| 347 |
|
| 348 |
|
| 349 | |
| 350 |
|
| 351 | |
| 352 | if ( $replyJSON ){
|
| 353 | reply_JSON($serverData);
|
| 354 | } else {
|
| 355 | resortResultArray($serverData);
|
| 356 | unset($serverData['Success']);
|
| 357 | $serverData['InternalRouter'] = ($serverData['InternalRouter'] ? "YES, single-port communication" : "NO, separate-port connections required");
|
| 358 | if ( $serverData['GamePort'] == 0 )
|
| 359 | $serverData['GamePort'] = "(unknown; no DTLS support available)";
|
| 360 | msg("\nTEST SUMMARY:\n");
|
| 361 | foreach($serverData as $field => $value)
|
| 362 | msg(str_pad("$field:", 18, " ")." {$value}\n");
|
| 363 | }
|
| 364 |
|
| 365 | |
| 366 |
|
| 367 | socket_close($socket); $socket = null; die(); |
| 368 |
|
| 369 | |