Issues (55)

scripts/roscon.php (3 issues)

1
<?php
2
3
/**
4
 * ~~summary~~
5
 *
6
 * ~~description~~
7
 *
8
 * PHP version 5.3
9
 *
10
 * @category  Net
11
 * @package   PEAR2_Net_RouterOS
12
 * @author    Vasil Rangelov <[email protected]>
13
 * @copyright 2011 Vasil Rangelov
14
 * @license   http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
15
 * @version   GIT: $Format:%x24Commit:%H%x24$
16
 * @link      http://pear2.php.net/PEAR2_Net_RouterOS
17
 */
18
19
/**
20
 * Used as a "catch all" for errors when connecting.
21
 */
22
use Exception as E;
23
24
/**
25
 * Used to register dependency paths, if needed.
26
 */
27
use PEAR2\Autoload;
28
29
/**
30
 * Used for coloring the output, if the "--colors" argument is specified.
31
 */
32
use PEAR2\Console\Color;
33
34
/**
35
 * Used for parsing the command line arguments.
36
 */
37
use PEAR2\Console\CommandLine;
38
39
/**
40
 * The whole application is around that.
41
 */
42
use PEAR2\Net\RouterOS;
43
44
/**
45
 * Used for error handling when connecting or receiving.
46
 */
47
use PEAR2\Net\Transmitter\SocketException as SE;
48
49
//Detect disallowed direct runs of either this file or "roscon".
50
if (PHP_SAPI !== 'cli') {
51
    $includedFiles = get_included_files();
52
    $rosconPos = array_search(
53
        dirname(__FILE__) . DIRECTORY_SEPARATOR . 'roscon',
54
        $includedFiles,
55
        true
56
    );
57
    if (false !== $rosconPos) {
58
        unset($includedFiles[$rosconPos]);
59
    }
60
61
    if (count($includedFiles) === 1) {
62
        header('Content-Type: text/plain;charset=UTF-8');
63
        echo <<<HEREDOC
64
For security reasons, this file can not be ran DIRECTLY, except from the
65
command line. It can be included however, even when not using the command line.
66
HEREDOC;
67
        return;
68
    }
69
}
70
71
//If there's no appropriate autoloader, add one
72
if (!class_exists('PEAR2\Net\RouterOS\Communicator', true)) {
73
    $cwd = getcwd();
74
    chdir(__DIR__);
75
76
    $composerAutoloaderPaths = array();
77
    $vendorDir = getenv('COMPOSER_VENDOR_DIR');
78
    if (false !== $vendorDir) {
79
        $composerAutoloaderPaths[] = $vendorDir . '/autoload.php';
80
        unset($vendorDir);
81
    }
82
    $composerAutoloaderPaths[] = '../vendor/autoload.php';
83
    $composerAutoloaderPaths[] = '../../../autoload.php';
84
    foreach ($composerAutoloaderPaths as $autoloaderPath) {
85
        $autoloader = stream_resolve_include_path($autoloaderPath);
86
        if (false !== $autoloader) {
87
            include_once $autoloader;
88
            if (class_exists('PEAR2\Net\RouterOS\Communicator', true)) {
89
                break;
90
            }
91
            $autoloader = false;
92
        }
93
    }
94
    unset($autoloaderPath, $composerAutoloaderPaths);
95
    if (false === $autoloader) {
96
        //PEAR2_Autoload, most probably installed globally.
97
        $autoloader = stream_resolve_include_path('PEAR2/Autoload.php');
98
        if (false !== $autoloader) {
99
            include_once $autoloader;
100
            Autoload::initialize(
101
                realpath('../src')
102
            );
103
            Autoload::initialize(
104
                realpath('../../Net_Transmitter.git/src')
105
            );
106
            Autoload::initialize(
107
                realpath('../../Console_Color.git/src')
108
            );
109
            Autoload::initialize(
110
                realpath('../../Console_CommandLine.git/src')
111
            );
112
        } else {
113
            $phpIniLocation = php_ini_loaded_file();
114
            $phpIncludePath = get_include_path();
115
            $defaultDirPyrus = __DIR__ . DIRECTORY_SEPARATOR . 'php';
116
            $defaultDirPear = __DIR__ . DIRECTORY_SEPARATOR . 'pear';
117
            fwrite(
118
                STDERR,
119
                <<<HEREDOC
120
No recognized autoloader is available.
121
Please install this package with Pyrus, PEAR or Composer.
122
123
If using PEAR or Pyrus, also install PEAR2_Autoload if it's not installed.
124
125
If it's already installed and yet this message appears,
126
make sure the folder with PHP files is in the list of paths in php.ini's
127
include_path directive.
128
129
The loaded php.ini is at
130
```
131
{$phpIniLocation}
132
```
133
134
and the computed include_path value is
135
```
136
{$phpIncludePath}
137
```
138
139
If you use the default settings for Pyrus, the folder to add is probably
140
```
141
{$defaultDirPyrus}
142
```
143
144
and for PEAR's default settings, the folder to add is probably
145
```
146
{$defaultDirPear}
147
```
148
149
HEREDOC
150
            );
151
            chdir($cwd);
152
            exit(10);
153
        }
154
    }
155
156
    chdir($cwd);
157
    unset($autoloader, $cwd);
158
}
159
160
//PEAR2_Console_CommandLine is bundled in the archive, but may not be
161
//available if this package was installed with a package manager.
162
if (!class_exists('PEAR2\Console\CommandLine', true)) {
163
    fwrite(
164
        STDERR,
165
        <<<HEREDOC
166
PEAR2_Console_CommandLine was not found.
167
Please install it with the package manager used to install PEAR2_Net_RouterOS.
168
(i.e. Pyrus, PEAR or Composer)
169
170
HEREDOC
171
    );
172
    exit(11);
173
}
174
175
// Locate the data dir, in preference as:
176
// 1. If outside of PHAR file
177
// 1.1. The data folder filled at install time by Pyrus.
178
// 1.2. The PHP_PEAR_DATA_DIR environment variable, if available.
179
// 1.3. The data folder filled at install time by PEAR.
180
// 2. The source layout's data folder, inside a channel/package subfolder
181
//    (in case this package itself is bundled by another Pyrus package)
182
// 3. The source layout's data folder, inside a package subfolder
183
//    (in case this package itself is bundled by another PEAR package)
184
// 4. The source layout's data folder (used with Composer or from Git).
185
$dataDirPaths = array();
186
if (!class_exists('Phar', false) || !Phar::running()) {
187
    $dataDirPaths[] = '@PEAR2_DATA_DIR@/@PACKAGE_CHANNEL@/@PACKAGE_NAME@';
188
    if (($pearDataDir = getenv('PHP_PEAR_DATA_DIR'))) {
189
        $dataDirPaths[] = $pearDataDir . '/@PACKAGE_NAME@';
190
    }
191
    $dataDirPaths[] = '@PEAR2_DATA_DIR@/@PACKAGE_NAME@';
192
}
193
$dataDirPaths[] = __DIR__ . '/../data/@PACKAGE_CHANNEL@/@PACKAGE_NAME@';
194
$dataDirPaths[] = __DIR__ . '/../data/@PACKAGE_NAME@';
195
$dataDirPaths[] = __DIR__ . '/../data';
196
$dataDir = false;
197
foreach ($dataDirPaths as $dataDirPath) {
198
    if (is_dir($dataDirPath)) {
199
        $dataDir = $dataDirPath;
200
        break;
201
    }
202
}
203
unset($dataDirPaths);
204
205
if (false === $dataDir) {
206
    fwrite(
207
        STDERR,
208
        <<<HEREDOC
209
Unable to find data dir.
210
211
HEREDOC
212
    );
213
    exit(11);
214
}
215
$consoleDefFile = is_file($dataDir . '/roscon.xml')
216
    ? $dataDir . '/roscon.xml'
217
    : false;
218
if (false === $consoleDefFile) {
219
    fwrite(
220
        STDERR,
221
        <<<HEREDOC
222
The console definition file (roscon.xml) was not found at the data dir, which
223
was found to be at
224
{$dataDir}
225
226
HEREDOC
227
    );
228
    exit(12);
229
}
230
231
$cmdParser = CommandLine::fromXmlFile($consoleDefFile);
232
try {
233
    $cmd = $cmdParser->parse();
234
} catch (CommandLine\Exception $e) {
235
    fwrite(
236
        STDERR,
237
        "Error when parsing command line: {$e->getMessage()}\n"
238
    );
239
    $cmdParser->displayUsage(13);
240
}
241
242
$comTimeout = null === $cmd->options['conTime']
243
    ? (null === $cmd->options['time']
244
            ? (int)ini_get('default_socket_timeout')
245
            : $cmd->options['time'])
246
    : $cmd->options['conTime'];
247
$cmd->options['time'] = $cmd->options['time'] ?: 3;
248
$comContext = null;
249
if ($cmd->options['crypto']) {
250
    $comContextOpts = array(
251
        'ssl' => array(
252
            'capture_peer_cert' => !$cmd->options['fingerprint']
253
                && function_exists('openssl_x509_fingerprint'),
254
            'verify_peer' => isset($cmd->options['caPath']),
255
            'verify_peer_name' => isset($cmd->options['caPath']),
256
        )
257
    );
258
    if ($cmd->options['caPath']) {
259
        $comContextOpts['ssl'][
260
            is_file($cmd->options['caPath']) ? 'cafile' : 'capath'
261
        ] = $cmd->options['caPath'];
262
    } elseif (!$cmd->options['fingerprint']) {
263
        $comContextOpts['ssl']['ciphers'] = 'ADH';
264
    }
265
266
    if ($cmd->options['fingerprint']) {
267
        $comContextOpts['ssl']['peer_fingerprint'] = array(
268
            'sha256' => $cmd->options['fingerprint']
269
        );
270
    }
271
272
    if ($cmd->options['ciphers']) {
273
        $comContextOpts['ssl']['ciphers'] = $cmd->options['ciphers'];
274
    }
275
276
    $comContext = stream_context_create($comContextOpts);
277
}
278
279
$cColors = array(
280
    'SEND' => '',
281
    'SENT' => '',
282
    'RECV' => '',
283
    'ERR'  => '',
284
    'NOTE' => '',
285
    ''     => ''
286
);
287
if ('auto' === $cmd->options['isColored']) {
288
    $cmd->options['isColored'] = ((strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN'
289
    || getenv('ANSICON_VER') != false)
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing getenv('ANSICON_VER') of type string to the boolean false. If you are specifically checking for a non-empty string, consider using the more explicit !== '' instead.
Loading history...
290
    && class_exists('PEAR2\Console\Color', true)) ? 'yes' : 'no';
291
}
292
if ('yes' === $cmd->options['isColored']) {
293
    if (class_exists('PEAR2\Console\Color', true)) {
294
        $cColors['SEND'] = new Color(
295
            Color\Fonts::PURPLE
296
        );
297
        $cColors['SENT'] = clone $cColors['SEND'];
298
        $cColors['SENT']->setStyles(Color\Styles::UNDERLINE, true);
299
        $cColors['RECV'] = new Color(
300
            Color\Fonts::GREEN
301
        );
302
        $cColors['ERR'] = new Color(
303
            Color\Fonts::WHITE,
304
            Color\Backgrounds::RED
305
        );
306
        $cColors['NOTE'] = new Color(
307
            Color\Fonts::BLUE,
308
            Color\Backgrounds::YELLOW
309
        );
310
        $cColors[''] = new Color();
311
312
        foreach ($cColors as $mode => $color) {
313
            $cColors[$mode] = ((string)$color) . "\033[K";
314
        }
315
    } else {
316
        fwrite(
317
            STDERR,
318
            <<<HEREDOC
319
Warning: Color was forced, but PEAR2_Console_Color is not available.
320
         Resuming with colors disabled.
321
322
HEREDOC
323
        );
324
    }
325
}
326
327
try {
328
    $com = new RouterOS\Communicator(
329
        $cmd->args['hostname'],
330
        $cmd->options['portNum'],
331
        false,
332
        $comTimeout,
333
        '',
334
        (string)$cmd->options['crypto'],
335
        $comContext
336
    );
337
} catch (E $e) {
338
    fwrite(STDERR, "Error upon connecting: {$e->getMessage()}\n");
339
    $previous = $e->getPrevious();
340
    if ($previous instanceof SE) {
341
        fwrite(
342
            STDERR,
343
            "Details: ({$previous->getSocketErrorNumber()}) "
344
            . $previous->getSocketErrorMessage() . "\n\n"
345
        );
346
    }
347
    if ($e instanceof RouterOS\SocketException
348
        && $e->getCode() === RouterOS\SocketException::CODE_CONNECTION_FAIL
349
    ) {
350
        $phpBin = defined('PHP_BINARY')
351
            ? PHP_BINARY
352
            : (PHP_BINDIR . DIRECTORY_SEPARATOR .
353
                (PHP_SAPI === 'cli' ? 'php' : 'php-cgi') .
354
                (stristr(PHP_OS, 'win') === false ? '' : '.exe'));
355
        fwrite(
356
            STDERR,
357
            <<<HEREDOC
358
Possible reasons:
359
360
1. You haven't enabled the API service at RouterOS or you've enabled it on a
361
   different TCP port.
362
   Make sure the "api" service at the "/ip service" menu is enabled,
363
   and with that same TCP port (8728 by default or 8729 for "api-ssl").
364
365
2. You've mistyped the IP and/or port.
366
   Check the IP and port you've specified are the ones you intended.
367
368
3. Your web server's IP is not in the list of subnets allowed to use the API.
369
   Check the "address" property at the "/ip service" menu.
370
   If it's empty, that's not the problem for sure. If it's non-empty however,
371
   make sure your IP is in that list, or is at least matched as part of an
372
   otherwise larger subnet.
373
374
4. The router is not reachable from your web server for some reason.
375
   Try to reach the router (!!!)from the web server(!!!) by other means
376
   (e.g. Winbox, ping) using the same IP, and if you're unable to reach it,
377
   check the network settings on your server, router and any intermediate nodes
378
   under your control that may affect the connection.
379
380
5. Your web server is configured to forbid that outgoing connection.
381
   If you're the web server administrator, check your web server's firewall
382
   settings. The binary you need to whitelist is probably
383
   ```
384
   {$phpBin}
385
   ```
386
387
   If you're on a hosting plan... Typically, shared hosts block all
388
   outgoing connections, but it's also possible that only connections to that
389
   port are blocked. Try to connect to a host on a popular port (21, 80, 443,
390
   etc.), and if successful, change the API service port to that port. If the
391
   connection fails even then, ask your host to configure their firewall so as
392
   to allow you to make outgoing connections to the ip:port you've set the API
393
   service on.
394
395
   Note that using the library may require a different binary to be
396
   whitelisted, depending on how PHP is running as.
397
398
6. The router has a firewall filter/mangle/nat rule that overrides the settings
399
   at the "/ip service" menu.
400
   Usually, those are rules in the "input" chain.
401
   Theoretically (rarely in practice), rules in the "prerouting", "dstnat",
402
   "output" and/or "postrouting" chains can also cause such an effect.
403
   By default, many RouterBOARD devices have a filter rule in the "input" chain
404
   that drops any incoming connections to the router from its WAN interface,
405
   so if your web server is not in the LAN, the connection may be dropped
406
   because of that.
407
   If that's the case, either disable that rule (not recommended), or
408
   explicitly whitelist the API port. You can whitelist the API port on all
409
   interfaces by issuing the following command from a terminal:
410
   ```
411
   /ip firewall filter add \
412
       place-before=[:pick [find where chain="input"] 0] \
413
       chain="input" action="accept" \
414
       protocol="tcp" dst-port=[/ip service get "api" "port"]
415
   ```
416
417
HEREDOC
418
        );
419
        if ($cmd->options['crypto']) {
420
            fwrite(
421
                STDERR,
422
                <<<HEREDOC
423
424
7. Encryption misconfiguration or impersonation attempt by an attacker.
425
   Check carefully the values and presense of encryption related options, i.e.
426
   the "--ca", "--ciphers" and "--fingerprint" options.
427
   If not using a certifcate, make sure to NOT specify any of those options,
428
   or explicitly include only ADH ciphers in the "--ciphers" option.
429
   If using a certificate, make sure to specify at least one of those options
430
   with a valid value. Also make sure the clock of this device and the router
431
   are accurate (check the date in particular), and that the certificate
432
   has not expired yet.
433
   If using a certificate and the "--ca" option, keep in mind the name is
434
   also checked (just like with certificates in a web browser). Make sure
435
   your certificate includes a subject alt name for the hostname you use to
436
   connect to the router.
437
   If using a certificate and the "--fingerprint" option, keep in mind that
438
   renewing a certificate changes the fingerprint.
439
440
   You can check details for the current certificate by checking the output of
441
   the following command from a terminal:
442
   ```
443
   /certificate print from=[/ip service get "api-ssl" "certificate"] detail
444
   ```
445
446
   If everything appears OK, there may be an attacker in your network trying
447
   to impersonate RouterOS.
448
449
HEREDOC
450
            );
451
        }
452
    }
453
    return;
454
}
455
if (null !== $cmd->args['username']) {
456
    try {
457
        if (!RouterOS\Client::login(
458
            $com,
459
            $cmd->args['username'],
460
            (string)$cmd->args['password'],
461
            $comTimeout
462
        )
463
        ) {
464
            fwrite(
465
                STDERR,
466
                <<<HEREDOC
467
Login refused. Possible reasons:
468
469
1. No such username.
470
   Make sure you have spelled it correctly.
471
472
2. The user does not have the "api" privilege.
473
   Check the permissions of the user's group at "/user group".
474
475
3. The user is not allowed to access the router from your web server's IP.
476
   Check the "address" property at the "/user" menu.
477
   If it's empty, that's not the problem for sure. If it's non-empty however,
478
   make sure your IP is in that list, or is at least matched as part of an
479
   otherwise larger subnet.
480
481
4. Mistyped password.
482
   Make sure you have spelled it correctly.
483
   If it contains spaces, don't forget to quote the whole password.
484
   If it contains non-ASCII characters, be careful of your locale.
485
   It must match that of the terminal you set your password on, or you must
486
   type the equivalent code points in your current locale, which may display as
487
   different characters.
488
489
HEREDOC
490
            );
491
            return;
492
        }
493
    } catch (RouterOS\SocketException $e) {
494
        fwrite(STDERR, "Error upon login: " . $e->getMessage());
495
        return;
496
    }
497
}
498
499
if ($cmd->options['verbose']) {
500
    $cSep = ' | ';
501
    $cColumns = array(
502
        'mode' => 4,
503
        'length' => 11,
504
        'encodedLength' => 12
505
    );
506
    $cColumns['contents'] = $cmd->options['size'] - 1//row length
507
            - array_sum($cColumns)
508
            - (3/*strlen($c_sep)*/ * count($cColumns));
509
    fwrite(
510
        STDOUT,
511
        implode(
512
            "\n",
513
            array(
514
                implode(
515
                    $cSep,
516
                    array(
517
                        str_pad(
518
                            'MODE',
519
                            $cColumns['mode'],
520
                            ' ',
521
                            STR_PAD_RIGHT
522
                        ),
523
                        str_pad(
524
                            'LENGTH',
525
                            $cColumns['length'],
526
                            ' ',
527
                            STR_PAD_BOTH
528
                        ),
529
                        str_pad(
530
                            'LENGTH',
531
                            $cColumns['encodedLength'],
532
                            ' ',
533
                            STR_PAD_BOTH
534
                        ),
535
                        ' CONTENTS'
536
                    )
537
                ),
538
                implode(
539
                    $cSep,
540
                    array(
541
                        str_repeat(' ', $cColumns['mode']),
542
                        str_pad(
543
                            '(decoded)',
544
                            $cColumns['length'],
545
                            ' ',
546
                            STR_PAD_BOTH
547
                        ),
548
                        str_pad(
549
                            '(encoded)',
550
                            $cColumns['encodedLength'],
551
                            ' ',
552
                            STR_PAD_BOTH
553
                        ),
554
                        ''
555
                    )
556
                ),
557
                implode(
558
                    '-|-',
559
                    array(
560
                        str_repeat('-', $cColumns['mode']),
561
                        str_repeat('-', $cColumns['length']),
562
                        str_repeat('-', $cColumns['encodedLength']),
563
                        str_repeat('-', $cColumns['contents'])
0 ignored issues
show
$cColumns['contents'] of type double is incompatible with the type integer expected by parameter $times of str_repeat(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

563
                        str_repeat('-', /** @scrutinizer ignore-type */ $cColumns['contents'])
Loading history...
564
                    )
565
                )
566
            )
567
        ) . "\n"
568
    );
569
570
    $cRegexWrap = '/([^\n]{1,' . ($cColumns['contents']) . '})/sS';
571
572
    $printWord = function (
573
        $mode,
574
        $word,
575
        $msg = ''
576
    ) use (
577
        $cSep,
578
        $cColumns,
579
        $cRegexWrap,
580
        $cColors
581
    ) {
582
        $wordFragments = preg_split(
583
            $cRegexWrap,
584
            $word,
585
            null,
586
            PREG_SPLIT_DELIM_CAPTURE
587
        );
588
        for ($i = 0, $l = count($wordFragments); $i < $l; $i += 2) {
589
            unset($wordFragments[$i]);
590
        }
591
        if ('' !== $cColors['']) {
592
            $wordFragments = str_replace("\033", "\033[27@", $wordFragments);
593
        }
594
595
        $isAbnormal = 'ERR' === $mode || 'NOTE' === $mode;
596
        if ($isAbnormal) {
597
            $details = str_pad(
598
                $msg,
599
                $cColumns['length'] + $cColumns['encodedLength'] + 3,
600
                ' ',
601
                STR_PAD_BOTH
602
            );
603
        } else {
604
            $length = strlen($word);
605
            $lengthBytes = RouterOS\Communicator::encodeLength($length);
606
            $encodedLength = '';
607
            for ($i = 0, $l = strlen($lengthBytes); $i < $l; ++$i) {
608
                $encodedLength .= str_pad(
609
                    dechex(ord($lengthBytes[$i])),
610
                    2,
611
                    '0',
612
                    STR_PAD_LEFT
613
                );
614
            }
615
616
            $details = str_pad(
617
                $length,
618
                $cColumns['length'],
619
                ' ',
620
                STR_PAD_LEFT
621
            ) .
622
            $cSep .
623
            str_pad(
624
                '0x' . strtoupper($encodedLength),
625
                $cColumns['encodedLength'],
626
                ' ',
627
                STR_PAD_LEFT
628
            );
629
        }
630
        fwrite(
631
            STDOUT,
632
            $cColors[$mode] .
633
            str_pad($mode, $cColumns['mode'], ' ', STR_PAD_RIGHT) .
634
            $cColors[''] .
635
            "{$cSep}{$details}{$cSep}{$cColors[$mode]}" .
636
            implode(
637
                "\n{$cColors['']}" .
638
                str_repeat(' ', $cColumns['mode']) .
639
                $cSep .
640
                implode(
641
                    ($isAbnormal ? '   ' : $cSep),
642
                    array(
643
                        str_repeat(' ', $cColumns['length']),
644
                        str_repeat(' ', $cColumns['encodedLength'])
645
                    )
646
                ) . $cSep . $cColors[$mode],
647
                $wordFragments
648
            ) . "\n{$cColors['']}"
649
        );
650
    };
651
} else {
652
    $printWord = function ($mode, $word, $msg = '') use ($cColors) {
653
        if ('' !== $cColors['']) {
654
            $word = str_replace("\033", "\033[27@", $word);
655
            $msg = str_replace("\033", "\033[27@", $msg);
656
        }
657
658
        if ('ERR' === $mode || 'NOTE' === $mode) {
659
            fwrite(STDERR, "{$cColors[$mode]}-- {$msg}");
660
            if ('' !== $word) {
661
                fwrite(STDERR, ": {$word}");
662
            }
663
            fwrite(STDERR, "{$cColors['']}\n");
664
        } elseif ('SENT' !== $mode) {
665
            fwrite(STDOUT, "{$cColors[$mode]}{$word}{$cColors['']}\n");
666
        }
667
    };
668
}
669
670
if ($com->getTransmitter()->isAvailable()) {
671
    $printWord('NOTE', '', 'Connection started');
672
673
    if ($cmd->options['crypto']
674
        && $comContextOpts['ssl']['capture_peer_cert']
675
    ) {
676
        $contextParams = $com->getTransmitter()->getContextParams();
677
        $cert = $contextParams['options']['ssl']['peer_certificate'];
678
        $printWord(
679
            'NOTE',
680
            openssl_x509_fingerprint($cert, 'sha256'),
681
            'Certificate fingerprint'
682
        );
683
    }
684
}
685
686
//Input/Output cycle
687
while (true) {
688
    $prevWord = null;
689
    $word = '';
690
    $words = array();
691
692
693
    if (!$com->getTransmitter()->isAvailable()) {
694
        $printWord('NOTE', '', 'Connection terminated');
695
        break;
696
    }
697
698
    //Input cycle
699
    while (true) {
700
        if ($cmd->options['verbose']) {
701
            fwrite(
702
                STDOUT,
703
                implode(
704
                    $cSep,
705
                    array(
706
                        $cColors['SEND'] .
707
                        str_pad('SEND', $cColumns['mode'], ' ', STR_PAD_RIGHT)
708
                        . $cColors[''],
709
                        str_pad(
710
                            '<prompt>',
711
                            $cColumns['length'],
712
                            ' ',
713
                            STR_PAD_LEFT
714
                        ),
715
                        str_pad(
716
                            '<prompt>',
717
                            $cColumns['encodedLength'],
718
                            ' ',
719
                            STR_PAD_LEFT
720
                        ),
721
                        ''
722
                    )
723
                )
724
            );
725
        }
726
727
        fwrite(STDOUT, (string)$cColors['SEND']);
728
729
        if ($cmd->options['multiline']) {
730
            while (true) {
731
                $line = stream_get_line(STDIN, PHP_INT_MAX, PHP_EOL);
732
                if (chr(3) === $line) {
733
                    break;
734
                }
735
                if ((chr(3) . chr(3)) === $line) {
736
                    $word .= chr(3);
737
                } else {
738
                    $word .=  $line . PHP_EOL;
739
                }
740
                if ($cmd->options['verbose']) {
741
                    fwrite(
742
                        STDOUT,
743
                        "\n{$cColors['']}" .
744
                        implode(
745
                            $cSep,
746
                            array(
747
                                str_repeat(' ', $cColumns['mode']),
748
                                str_repeat(' ', $cColumns['length']),
749
                                str_repeat(' ', $cColumns['encodedLength']),
750
                                ''
751
                            )
752
                        )
753
                        . $cColors['SEND']
754
                    );
755
                }
756
            }
757
            if ('' !== $word) {
758
                $word = substr($word, 0, -strlen(PHP_EOL));
759
            }
760
        } else {
761
            $word = stream_get_line(STDIN, PHP_INT_MAX, PHP_EOL);
762
        }
763
764
        if ($cmd->options['verbose']) {
765
            fwrite(STDOUT, "\n");
766
        }
767
        fwrite(STDOUT, (string)$cColors['']);
768
769
        $words[] = $word;
770
        if ('w' === $cmd->options['commandMode']) {
771
            break;
772
        }
773
        if ('' === $word) {
774
            if ('s' === $cmd->options['commandMode']) {
775
                break;
776
            } elseif ('' === $prevWord) {//'e' === $cmd->options['commandMode']
777
                array_pop($words);
778
                break;
779
            }
780
        }
781
        $prevWord = $word;
782
        $word = '';
783
    }
784
785
    //Input flush
786
    foreach ($words as $word) {
787
        try {
788
            $com->sendWord($word);
789
            $printWord('SENT', $word);
790
        } catch (SE $e) {
791
            if (0 === $e->getFragment()) {
792
                $printWord('ERR', '', 'Failed to send word');
793
            } else {
794
                $printWord(
795
                    'ERR',
796
                    substr($word, 0, $e->getFragment()),
0 ignored issues
show
It seems like $e->getFragment() can also be of type resource and string; however, parameter $length of substr() does only seem to accept integer|null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

796
                    substr($word, 0, /** @scrutinizer ignore-type */ $e->getFragment()),
Loading history...
797
                    'Partial word sent'
798
                );
799
            }
800
        }
801
    }
802
803
    //Output cycle
804
    while (true) {
805
        if (!$com->getTransmitter()->isAvailable()) {
806
            break;
807
        }
808
809
        if (!$com->getTransmitter()->isDataAwaiting($cmd->options['time'])) {
810
            $printWord('NOTE', '', 'Receiving timed out');
811
            break;
812
        }
813
814
        try {
815
            $word = $com->getNextWord();
816
            $printWord('RECV', $word);
817
818
            if ('w' === $cmd->options['replyMode']
819
                || ('s' === $cmd->options['replyMode'] && '' === $word)
820
            ) {
821
                break;
822
            }
823
        } catch (SE $e) {
824
            if ('' === $e->getFragment()) {
825
                $printWord('ERR', '', 'Failed to receive word');
826
            } else {
827
                $printWord('ERR', $e->getFragment(), 'Partial word received');
828
            }
829
            break;
830
        } catch (RouterOS\NotSupportedException $e) {
831
            $printWord('ERR', $e->getValue(), 'Unsupported control byte');
832
            break;
833
        } catch (E $e) {
834
            $printWord('ERR', (string)$e, 'Unknown error');
835
            break;
836
        }
837
    }
838
}
839