clrghouz/app/Classes/Protocol/Binkp.php

1482 lines
43 KiB
PHP

<?php
namespace App\Classes\Protocol;
use App\Exceptions\FileGrewException;
use Carbon\Carbon;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use League\Flysystem\UnreadableFileEncountered;
use App\Classes\Crypt;
use App\Classes\Protocol as BaseProtocol;
use App\Classes\Sock\SocketClient;
use App\Classes\Sock\SocketException;
use App\Models\Address;
final class Binkp extends BaseProtocol
{
private const LOGKEY = 'PB-';
/* CONSTS */
public const PORT = 24554;
/** protocol text **/
private const PROT = 'binkp';
/** version implemented */
private const VERSION = '1.1';
/** block size - compressed files can only use a blocksize of 0x3fff */
private const BLOCKSIZE = 0x1000;
/** session timeout secs */
private const TIMEOUT_TIME = 300;
/** max block size */
private const MAX_BLKSIZE = 0x7fff;
/* FEATURES */
/** COMPRESS mode */
public const F_COMP = 1<<0;
/** CHAT mode - not implemented */
public const F_CHAT = 1<<1;
/** Crypt mode */
public const F_CRYPT = 1<<2;
/** Multi-Batch mode */
public const F_MULTIBATCH = 1<<3;
/** CRAM-MD5 mode */
public const F_MD = 1<<4;
/** Force MD5 passwords */
public const F_MDFORCE = 1<<5;
/** http://ftsc.org/docs/fsp-1027.001: No-dupes mode - negotiated, implies NR mode */
public const F_NODUPE = 1<<6;
/** http://ftsc.org/docs/fsp-1027.001: Asymmetric ND mode */
public const F_NODUPEA = 1<<7;
/** http://ftsc.org/docs/fsp-1028.001: Non-Reliable mode */
public const F_NOREL = 1<<8;
/** Multi-Password mode - not implemented */
public const F_MULTIPASS = 1<<9;
/* BINKP MESSAGES */
/** No available data */
private const BPM_NONE = 99;
/** Binary data */
private const BPM_DATA = 98;
/** Site information */
private const BPM_NUL = 0;
/** List of addresses */
private const BPM_ADR = 1;
/** Session password */
private const BPM_PWD = 2;
/** File information */
private const BPM_FILE = 3;
/** Password was acknowledged (data ignored) */
private const BPM_OK = 4;
/** End Of Batch (data ignored) */
private const BPM_EOB = 5;
/** File received */
private const BPM_GOTSKIP = 6;
/** Misc errors */
private const BPM_ERR = 7;
/** All AKAs are busy */
private const BPM_BSY = 8;
/** Get a file from offset */
private const BPM_GET = 9;
/** Skip a file (RECEIVE LATER) */
private const BPM_SKIP = 10;
/** Reserved for later */
private const BPM_RESERVED = 11;
/** For chat */
private const BPM_CHAT = 12;
/** Minimal message type value */
private const BPM_MIN = self::BPM_NUL;
/** Maximal message type value */
private const BPM_MAX = self::BPM_CHAT;
/* SESSION STATE */
/** 0000 0001 - Are we in initialise mode */
private const SE_INIT = 1<<0;
/** 0000 0010 - Have we sent our EOB */
private const SE_SENTEOB = 1<<1;
/** 0000 0100 - Have we received EOB */
private const SE_RECVEOB = 1<<2;
/** 0000 1000 - Delay sending M_EOB message until remote's one if remote is binkp/1.0 and pretends to have FREQ on us */
private const SE_DELAYEOB = 1<<3;
/** 0001 0000 - Wait for GET before sending a file */
private const SE_WAITGET = 1<<4;
/** 0010 0000 - We are waiting for a GOT from the remote */
private const SE_WAITGOT = 1<<5;
/** 0100 0000 - Are we sending a file */
private const SE_SENDFILE = 1<<6;
/** 1000 0000 - We have no more files to send */
private const SE_NOFILES = 1<<7;
/* CLASS VARS */
/** The MD5 challenge with the remote system */
private string $md_challenge;
private int $is_msg;
/** Messages In Batch (MIB 3 :) */
private int $mib;
private int $rc;
private int $error;
private int $rx_size;
private string $rx_buf = '';
private ?Collection $mqueue;
private string $tx_buf;
private int $tx_ptr;
private int $tx_left;
private string $comp_mode = '';
/**
* @var Crypt Inbound encryption
*/
private Crypt $crypt_in;
/**
* @var Crypt Outbound encryption
*/
private Crypt $crypt_out;
/**
* Incoming BINKP session
*
* @param SocketClient $client
* @return int|null
* @throws SocketException
* @throws \Exception
*/
public function onConnect(SocketClient $client): ?int
{
// If our parent returns a PID, we've forked
if (! parent::onConnect($client)) {
Log::withContext(['pid'=>getmypid()]);
// @todo If we can use SESSION_EMSI set an object class value that in BINKP of SESSION_BINKP, and move this method to the parent class
$this->session(self::SESSION_BINKP,$client,(new Address));
$this->client->close();
exit(0);
}
return NULL;
}
/**
* BINKD handshake
*
* @throws \Exception
*/
private function binkp_hs(): void
{
Log::debug(sprintf('%s:+ Starting BINKP handshake',self::LOGKEY));
if (! $this->originate && $this->capGet(self::F_MD,self::O_WANT)) {
$random_key = random_bytes(8);
$this->md_challenge = md5($random_key,TRUE);
$this->msgs(self::BPM_NUL,sprintf('OPT CRAM-MD5-%s',md5($random_key)));
}
$this->msgs(self::BPM_NUL,sprintf('SYS %s',$this->setup->system->name));
$this->msgs(self::BPM_NUL,sprintf('ZYZ %s',$this->setup->system->sysop));
$this->msgs(self::BPM_NUL,sprintf('LOC %s',$this->setup->system->location));
$this->msgs(self::BPM_NUL,sprintf('NDL %d,TCP,BINKP',$this->client->speed));
$this->msgs(self::BPM_NUL,sprintf('TIME %s',Carbon::now()->toRfc2822String()));
$this->msgs(self::BPM_NUL,
sprintf('VER %s-%s %s/%s',config('app.name'),$this->setup->version,self::PROT,self::VERSION));
if ($this->originate) {
$opt = $this->capGet(self::F_NOREL,self::O_WANT) ? ' NR' : '';
$opt .= $this->capGet(self::F_NODUPE,self::O_WANT) ? ' ND' : '';
$opt .= $this->capGet(self::F_NODUPEA,self::O_WANT) ? ' NDA': '';
$opt .= $this->capGet(self::F_MULTIBATCH,self::O_WANT) ? ' MB' : '';
$opt .= $this->capGet(self::F_CHAT,self::O_WANT) ? ' CHAT' : '';
$opt .= $this->capGet(self::F_COMP,self::O_WANT) ? ' EXTCMD GZ' : '';
$opt .= $this->capGet(self::F_COMP,self::O_WANT) && $this->capGet(self::F_COMP,self::O_EXT) ? ' BZ2' : '';
$opt .= $this->capGet(self::F_CRYPT,self::O_WANT) ? ' CRYPT' : '';
if (strlen($opt))
$this->msgs(self::BPM_NUL,sprintf('OPT%s',$opt));
}
// If we are originating, we'll show the remote our address in the same network
if ($this->originate) {
$addresses = $this->our_addresses();
$this->msgs(self::BPM_ADR,$addresses->pluck('ftn')->join(' '));
}
}
/**
* @return int
*/
private function binkp_hsdone(): bool
{
Log::debug(sprintf('%s:+ BINKP handshake complete',self::LOGKEY));
// If the remote doesnt provide a password, or in MD5 mode, then we cant use CRYPT
if (! $this->optionGet(self::O_PWD) || (! $this->capGet(self::F_MD,self::O_WE)))
$this->capSet(self::F_CRYPT,self::O_NO);
if ($this->capGet(self::F_CRYPT,self::O_WE)) {
$this->capSet(self::F_CRYPT,self::O_YES);
Log::info(sprintf('%s:- CRYPT mode initialised',self::LOGKEY));
if ($this->originate) {
$this->crypt_out = new Crypt($this->node->password);
$this->crypt_in = new Crypt('-'.$this->node->password);
} else {
$this->crypt_in = new Crypt($this->node->password);
$this->crypt_out = new Crypt('-'.$this->node->password);
}
}
// @todo Implement max incoming sessions and max incoming session for the same node
// We have no mechanism to support chat
if ($this->capGet(self::F_CHAT,self::O_THEY))
Log::warning(sprintf('%s:/ The remote wants to chat, but we cant do chat',self::LOGKEY));
/*
if ($this->capGet(self::F_CHAT,self::O_WE))
$this->capSet(self::F_CHAT,self::O_YES);
*/
// No dupes mode is preferred on BINKP 1.1
if ($this->capGet(self::F_NODUPE,self::O_WE) || ($this->originate && $this->capGet(self::F_NOREL,self::O_WANT) && $this->node->get_versionint() > 100)) {
Log::debug(sprintf('%s:/ NR mode enabled, because we are in NDA mode, or I want NDA and the remote is version [%d]',self::LOGKEY,$this->node->get_versionint()));
$this->capSet(self::F_NOREL,self::O_YES);
}
$this->capSet(self::F_MULTIBATCH,(($this->node->get_versionint() > 100) || $this->capGet(self::F_MULTIBATCH,self::O_WE)) ? self::O_YES : self::O_NO);
if ($this->node->get_versionint() > 100)
$this->sessionClear(self::SE_DELAYEOB);
$this->mib = 0;
$this->sessionClear(self::SE_INIT);
Log::info(sprintf('%s:= Session: BINKP/%d.%d - NR:%d, ND:%d, NDA:%d, MD:%d, MB:%d, CR:%d, CO:%d, CH:%d',
self::LOGKEY,
$this->node->ver_major,
$this->node->ver_minor,
$this->capGet(self::F_NOREL,0xFF),
$this->capGet(self::F_NODUPE,0xFF),
$this->capGet(self::F_NODUPEA,0xFF),
$this->capGet(self::F_MD,0xFF),
$this->capGet(self::F_MULTIBATCH,0xFF),
$this->capGet(self::F_CRYPT,0xFF),
$this->capGet(self::F_COMP,0xFF),
$this->capGet(self::F_CHAT,0xFF),
));
return TRUE;
}
private function binkp_init(): int
{
$this->sessionSet(self::SE_INIT);
$this->is_msg = -1;
$this->mib = 0;
$this->error = 0;
$this->mqueue = collect();
$this->rx_size = -1;
$this->tx_buf = '';
$this->tx_left = 0; // @todo can we replace this with strlen($tx_buf)?
$this->tx_ptr = 0; // @todo is this required?
// Setup our default capabilities
$this->md_challenge = '';
// We cant do chat
$this->capSet(self::F_CHAT,self::O_NO);
// Compression
if ($this->setup->optionGet(self::F_COMP,'binkp_options'))
$this->capSet(self::F_COMP,self::O_WANT|self::O_EXT);
// CRAM-MD5 session
if ($this->setup->optionGet(self::F_MD,'binkp_options')) {
$this->capSet(self::F_MD,self::O_WANT);
if ($this->setup->optionGet(self::F_MDFORCE,'binkp_options'))
$this->capSet(self::F_MD,self::O_NEED);
}
// Crypt Mode
if ($this->setup->optionGet(self::F_CRYPT,'binkp_options'))
$this->capSet(self::F_CRYPT,self::O_WANT);
// Multibatch
if ($this->setup->optionGet(self::F_MULTIBATCH,'binkp_options'))
$this->capSet(self::F_MULTIBATCH,self::O_WANT);
// Non reliable mode
if ($this->setup->optionGet(self::F_NOREL,'binkp_options')) {
$this->capSet(self::F_NOREL,self::O_WANT);
// No dupes
if ($this->setup->optionGet(self::F_NODUPE,'binkp_options')) {
$this->capSet(self::F_NODUPE,self::O_WANT);
// No dupes asymmetric
if ($this->setup->optionGet(self::F_NODUPEA,'binkp_options'))
$this->capSet(self::F_NODUPEA,self::O_WANT);
}
}
return self::OK;
}
/**
* Receive data from the remote
*
* @throws \Exception
*/
private function binkp_recv(): bool
{
$blksz = $this->rx_size === -1 ? BinkpMessage::BLK_HDR_SIZE : $this->rx_size;
Log::debug(sprintf('%s:+ BINKP receive, reading [%d] chars',self::LOGKEY,$blksz));
if ($blksz !== 0) {
try {
Log::debug(sprintf('%s:- We need [%d] more chars, buffer currently has [%d] chars',self::LOGKEY,$blksz,strlen($this->rx_buf)));
$rx_buf = $this->client->read(0,$blksz-strlen($this->rx_buf));
Log::debug(sprintf('%s:- Got [%d] more chars for the read buffer',self::LOGKEY,strlen($rx_buf)));
} catch (SocketException $e) {
if ($e->getCode() === 11) {
// @todo We maybe should count these and abort if there are too many?
if ($this->DEBUG)
Log::debug(sprintf('%s:- Got a socket EAGAIN',self::LOGKEY));
return TRUE;
}
$this->error = 1;
Log::error(sprintf('%s:! Reading we got an EXCEPTION [%d-%s]',self::LOGKEY,$e->getCode(),$e->getMessage()));
return FALSE;
}
if (strlen($rx_buf) === 0) {
// @todo Check that this is correct.
Log::debug(sprintf('%s:- Was the socket closed by the remote?',self::LOGKEY));
$this->error = -2;
return FALSE;
}
$this->rx_buf .= ($this->capGet(self::F_CRYPT,self::O_YES)) ? $this->crypt_in->decrypt($rx_buf) : $rx_buf;
}
Log::debug(sprintf('%s:- Read buffer has [%d] chars to process.',self::LOGKEY,strlen($this->rx_buf)));
/* Received complete block */
if (strlen($this->rx_buf) === $blksz) {
/* Header */
if ($this->rx_size === -1 ) {
$this->is_msg = ord(substr($this->rx_buf,0,1)) >> 7;
// If compression is used, then this needs to be &0x3f, since the 2nd high bit is the compression flag
// @todo Need to see what happens, if we are receiving a block higher than 0x3fff. Possible?
$this->rx_size = ((ord(substr($this->rx_buf,0,1))&0x7f) << 8)+ord(substr($this->rx_buf,1,1));
Log::debug(sprintf('%s:- BINKP receive HEADER, is_msg [%d], rx_size [%d]',self::LOGKEY,$this->is_msg,$this->rx_size));
if ($this->rx_size === 0)
goto ZeroLen;
$rc = TRUE;
/* Next block */
} else {
ZeroLen:
if ($this->is_msg) {
$this->mib++;
/* Handle zero length block */
if ($this->rx_size === 0 ) {
Log::debug(sprintf('%s:- Received a ZERO length msg - dropped',self::LOGKEY));
$this->rx_size = -1;
$this->rx_buf = '';
return TRUE;
}
if ($this->DEBUG)
Log::debug(sprintf('%s: - binkp_recv BUFFER [%d]',self::LOGKEY,strlen($this->rx_buf)));
$msg = ord(substr($this->rx_buf,0,1));
if ($msg > self::BPM_MAX) {
Log::error(sprintf('%s:! Unknown message received [%d] (%d-%s)',self::LOGKEY,$rc,strlen($this->rx_buf),$this->rx_buf));
$rc = TRUE;
} else {
$data = substr($this->rx_buf,1);
switch ($msg) {
case self::BPM_ADR:
Log::debug(sprintf('%s:- ADR:Address [%s]',self::LOGKEY,$data));
$rc = $this->M_adr($data);
break;
case self::BPM_EOB:
Log::debug(sprintf('%s:- EOB:We got an EOB message with [%d] chars in the buffer',self::LOGKEY,strlen($data)));
$rc = $this->M_eob($data);
break;
case self::BPM_NUL:
Log::debug(sprintf('%s:- NUL:Message [%s]',self::LOGKEY,$data));
$rc = $this->M_nul($data);
break;
case self::BPM_PWD:
Log::debug(sprintf('%s:- PWD:We got a password [%s]',self::LOGKEY,$data));
$rc = $this->M_pwd($data);
break;
case self::BPM_ERR:
Log::debug(sprintf('%s:- ERR:We got an error [%s]',self::LOGKEY,$data));
$rc = $this->M_err($data);
break;
case self::BPM_FILE:
Log::debug(sprintf('%s:- FIL:We are receiving a file [%s]',self::LOGKEY,$data));
$rc = $this->M_file($data);
break;
case self::BPM_GET:
Log::debug(sprintf('%s:- GET:We are sending a file [%s]',self::LOGKEY,$data));
$rc = $this->M_get($data);
break;
case self::BPM_GOTSKIP:
Log::debug(sprintf('%s:- GOT:Remote received, or already has a file [%s]',self::LOGKEY,$data));
$rc = $this->M_gotskip($data);
break;
case self::BPM_OK:
Log::debug(sprintf('%s:- OK:Got an OK [%s]',self::LOGKEY,$data));
$rc = $this->M_ok($data);
break;
case self::BPM_CHAT:
Log::debug(sprintf('%s:- CHT:Remote sent a message [%s]',self::LOGKEY,$data));
$rc = $this->M_chat($data);
break;
default:
Log::error(sprintf('%s:! BINKP command not implemented [%d]',self::LOGKEY,$rc));
$rc = TRUE;
}
}
} else {
if ($this->recv->fd) {
try {
$this->recv->write($this->rx_buf);
} catch (FileGrewException $e) {
// Retry the file without compression
Log::error(sprintf('%s:! %s',self::LOGKEY,$e->getMessage()));
$this->msgs(self::BPM_GET,sprintf('%s %ld NZ',$this->recv->name_size_time,$this->recv->filepos));
} catch (\Exception $e) {
Log::error(sprintf('%s:! %s',self::LOGKEY,$e->getMessage()));
$this->recv->close();
$this->msgs(self::BPM_SKIP,$this->recv->name_size_time);
}
$rc = TRUE;
if ($this->recv->filepos === $this->recv->size) {
Log::info(sprintf('%s:- Finished receiving file [%s] with size [%d]',self::LOGKEY,$this->recv->name,$this->recv->size));
$this->msgs(self::BPM_GOTSKIP,$this->recv->name_size_time);
$this->recv->close();
}
} else {
Log::critical(sprintf('%s:- Ignoring data block, we dont have a received FD open?', self::LOGKEY));
$rc = TRUE;
}
}
$this->rx_size = -1;
}
$this->rx_buf = '';
} else {
$rc = TRUE;
}
if ($this->DEBUG)
Log::debug(sprintf('%s:= binkp_recv [%d]',self::LOGKEY,$rc));
return $rc;
}
/**
* @throws \Exception
*/
private function binkp_send(): int
{
Log::debug(sprintf('%s:+ BINKP send, TX buffer has [%d] chars (%d), and [%d] messages queued',self::LOGKEY,strlen($this->tx_buf),$this->tx_left,$this->mqueue->count()));
if ($this->tx_left === 0 ) { /* tx buffer is empty */
$this->tx_ptr = 0;
if ($this->mqueue->count()) { /* there are unsent messages */
while ($msg=$this->mqueue->shift()) {
if ($msg instanceof BinkpMessage) {
if (($msg->len+$this->tx_left) > self::MAX_BLKSIZE) {
Log::alert(sprintf('%s:! MSG [%d] would overflow our buffer [%d]',self::LOGKEY,$msg->len,$this->tx_left));
break;
}
Log::debug(sprintf('%s:- TX buffer empty, adding [%d] chars from the queue',self::LOGKEY,$msg->len));
$this->tx_buf .= $msg->msg;
$this->tx_left += $msg->len;
} else {
$this->tx_buf .= $msg;
$this->tx_left += strlen($msg);
}
}
} elseif ($this->sessionGet(self::SE_SENDFILE) && $this->send->fd && (! $this->sessionGet(self::SE_WAITGET))) {
$data = '';
try {
$buf = $this->send->read(self::BLOCKSIZE);
} catch (UnreadableFileEncountered) {
$this->send->close(FALSE);
$this->sessionClear(self::SE_SENDFILE);
} catch (\Exception $e) {
Log::error(sprintf('%s:! BINKP send unexpected ERROR [%s]',self::LOGKEY,$e->getMessage()));
}
if ($buf) {
$data = BinkpMessage::mkheader(strlen($buf));
$data .= $buf;
if ($this->capGet(self::F_CRYPT,self::O_YES)) {
$enc = $this->crypt_out->encrypt($data);
$this->tx_buf .= $enc;
$this->tx_left = strlen($enc);
} else {
$this->tx_buf .= $data;
$this->tx_left = strlen($buf)+BinkpMessage::BLK_HDR_SIZE;
}
}
// @todo should this be less than BLOCKSIZE? Since a read could return a blocksize and it could be the end of the file?
if ($this->send->filepos === $this->send->size) {
$this->sessionSet(self::SE_WAITGOT);
$this->sessionClear(self::SE_SENDFILE);
}
}
} else {
try {
Log::debug(sprintf('%s:- Sending [%d] chars to remote: tx_buf [%d], tx_ptr [%d]',self::LOGKEY,$this->tx_left,strlen($this->tx_buf),$this->tx_ptr));
$rc = $this->client->send(substr($this->tx_buf,$this->tx_ptr,$this->tx_left),self::TIMEOUT_TIME);
Log::info(sprintf('%s:- Sent [%d] chars to remote',self::LOGKEY,$rc));
} catch (\Exception $e) {
if ($e->getCode() === 11) {
Log::error(sprintf('%s:! Got a socket EAGAIN',self::LOGKEY));
return 1;
}
Log::error(sprintf('%s:! Sending we got an EXCEPTION [%d-%s]',self::LOGKEY,$e->getCode(),$e->getMessage()));
return 0;
}
$this->tx_ptr += $rc;
$this->tx_left -= $rc;
if (! $this->tx_left) {
$this->tx_buf = '';
$this->tx_ptr = 0;
}
}
return 1;
}
private function file_parse(string $str): ?array
{
$name = $this->strsep($str,' ');
$size = (int)$this->strsep($str,' ');
$time = (int)$this->strsep($str,' ');
$offs = (int)$this->strsep($str,' ');
$flags = $this->strsep($str,' ');
if ($name && $size && $time) {
return [
'file'=>['name'=>$name,'size'=>$size,'mtime'=>$time],
'offs'=>$offs,
'flags'=>$flags,
];
}
return NULL;
}
/**
* Add a BINKP control message to the queue
*
* @param string $id
* @param string $msg_body
* @return void
*/
private function msgs(string $id,string $msg_body): void
{
Log::debug(sprintf('%s:+ Queueing message to remote [%d:%s]',self::LOGKEY,$id,$msg_body));
$msg = new BinkpMessage($id,$msg_body);
// If encryption is enabled, we need to queue the encrypted version of the message
// @todo rework this so queue only has data, not objects
if ($this->capGet(self::F_CRYPT,self::O_YES)) {
$enc = $this->crypt_out->encrypt($msg->msg);
$this->mqueue->push($enc);
} else {
$this->mqueue->push($msg);
}
$this->mib++;
}
/**
* @throws \Exception
*/
private function M_adr(string $buf): bool
{
$buf = $this->skip_blanks($buf);
$rc = 0;
while ($rem_aka=$this->strsep($buf,' ')) {
try {
if (! ($o=Address::findFTN($rem_aka,FALSE,NULL,TRUE))) {
Log::alert(sprintf('%s:? AKA is UNKNOWN [%s]',self::LOGKEY,$rem_aka));
$this->node->ftn_other = $rem_aka;
continue;
} else if (! $o->active) {
Log::alert(sprintf('%s:/ AKA is not active [%s], ignoring',self::LOGKEY,$rem_aka));
continue;
} else {
Log::info(sprintf('%s:- Got AKA [%s]',self::LOGKEY,$rem_aka));
}
} catch (\Exception $e) {
Log::error(sprintf('%s:! AKA is INVALID [%s] (%s)',self::LOGKEY,$rem_aka,$e->getMessage()));
$this->msgs(self::BPM_ERR,sprintf('Bad address %s',$rem_aka));
$this->rc = self::S_FAILURE;
return FALSE;
}
// Check if the remote has our AKA
if ($this->setup->system->addresses->pluck('ftn')->search($rem_aka) !== FALSE) {
Log::error(sprintf('%s:! Remote\'s AKA is mine [%s]?',self::LOGKEY,$rem_aka));
$this->msgs(self::BPM_ERR,sprintf('Sorry that is my AKA [%s], who are you?',$rem_aka));
$this->rc = self::S_FAILURE;
return FALSE;
}
// @todo lock nodes
$this->node->ftn = $o;
$rc = $this->node->aka_num;
}
if ($rc === 0) {
Log::error(sprintf('%s:! All AKAs [%d] are busy',self::LOGKEY,$this->node->aka_num));
$this->msgs( self::BPM_BSY,'All AKAs are busy, nothing to do :(');
$this->rc = self::S_BUSY;
return FALSE;
}
if ($this->originate) {
if (! $this->node->originate_check()) {
Log::error(sprintf('%s:! We didnt get who we called?',self::LOGKEY));
$this->msgs( self::BPM_ERR,'Sorry, you are not who I expected');
$this->rc = self::S_FAILURE;
return 0;
}
// Add our mail to the queue if we have authenticated
if ($this->node->aka_authed)
foreach ($this->node->aka_remote_authed as $ao) {
$this->send->mail($ao);
$this->send->files($ao);
}
$this->msgs(self::BPM_NUL,sprintf('TRF %lu %lu',$this->send->mail_size,$this->send->files_size));
if ($this->md_challenge) {
Log::info(sprintf('%s:! Sending MD5 challenge',self::LOGKEY));
$this->msgs(self::BPM_PWD,sprintf('CRAM-MD5-%s',$this->node->get_md5chal($this->md_challenge)));
} elseif ($this->capGet(self::F_MD,self::O_NEED)) {
Log::error(sprintf('%s:! Node wants plaintext, but we insist on MD5 challenges',self::LOGKEY));
$this->msgs(self::BPM_ERR,'Can\'t use plaintext password');
$this->rc = self::S_FAILURE;
return 0;
} else {
Log::info(sprintf('%s:! Sending plain text password',self::LOGKEY));
$this->msgs(self::BPM_PWD,$this->node->password ?: '');
}
}
if (! $this->node->aka_num)
$this->optionClear(self::O_PWD);
else
$this->optionSet(self::O_PWD);
// If we are not the originator, we'll show our addresses in common.
if (! $this->originate)
$this->msgs(self::BPM_ADR,$this->our_addresses()->pluck('ftn')->join(' '));
return TRUE;
}
private function M_chat(string $buf): bool
{
if ($this->capGet(self::F_CHAT,self::O_YES)) {
Log::error(sprintf('%s:! We cannot do chat',self::LOGKEY));
} else {
Log::error(sprintf('%s:! We got a chat message, but chat is disabled (%s)',self::LOGKEY,strlen($buf)),['buf'=>$buf]);
}
return TRUE;
}
/**
* We received EOB from the remote.
*
* @throws \Exception
*/
private function M_eob(string $buf): bool
{
if ($this->recv->fd) {
Log::info(sprintf('%s:= Closing receiving file.',self::LOGKEY));
$this->recv->close();
}
$this->sessionSet(self::SE_RECVEOB);
$this->sessionClear(self::SE_DELAYEOB);
if (! $this->send->total_count && $this->sessionGet(self::SE_NOFILES)) {
// Add our mail to the queue if we have authenticated
if ($this->node->aka_authed)
foreach ($this->node->aka_remote_authed as $ao) {
Log::debug(sprintf('%s:- Checking for any new mail and files to [%s]',self::LOGKEY,$ao->ftn));
$this->send->mail($ao);
$this->send->files($ao);
}
Log::debug(sprintf('%s:- We have [%d] items to send to [%s]',self::LOGKEY,$this->send->total_count,$ao->ftn));
if ($this->send->total_count)
$this->sessionClear(self::SE_NOFILES|self::SE_SENTEOB);
}
return TRUE;
}
/**
* @throws \Exception
*/
private function M_err(string $buf): bool
{
Log::error(sprintf('%s:! We got an error, there are [%d] chars in the buffer (%s)',self::LOGKEY,strlen($buf),$buf));
$this->error_close();
$this->rc = self::S_FAILURE;
return TRUE;
}
/**
* @throws \Exception
*/
private function M_file(string $buf): bool
{
Log::info(sprintf('%s:+ About to receive a file [%s]',self::LOGKEY,$buf));
if ($this->sessionGet(self::SE_SENTEOB|self::SE_RECVEOB))
$this->sessionClear(self::SE_SENTEOB);
$this->sessionClear(self::SE_RECVEOB);
if ($this->recv->fd)
$this->recv->close();
if (! $file=$this->file_parse($buf)) {
Log::error(sprintf('%s:! UNPARSABLE file info [%s]',self::LOGKEY,$buf));
$this->msgs(self::BPM_ERR,sprintf('M_FILE: unparsable file info: "%s", what are you on?',$buf));
if ($this->sessionGet(self::SE_SENDFILE))
$this->send->close(FALSE);
$this->rc = self::S_FAILURE;
return FALSE;
}
// In NR mode, when we got -1 for the file offsite, the reply to our get will confirm our requested offset.
if ($this->recv->name && ! strncasecmp(Arr::get($file,'file.name'),$this->recv->name,self::MAX_PATH)
&& $this->recv->mtime === Arr::get($file,'file.mtime')
&& $this->recv->size === Arr::get($file,'file.size')
&& $this->recv->filepos === $file['offs'])
{
$this->recv->open($this->node->address,$file['offs']<0,$file['flags']);
return TRUE;
}
$this->recv->new($file['file']);
try {
switch ($this->recv->open($this->node->address,$file['offs']<0,$file['flags'])) {
case self::FOP_ERROR:
Log::error(sprintf('%s:! File ERROR',self::LOGKEY));
case self::FOP_SUSPEND:
case self::FOP_SKIP:
Log::info(sprintf('%s:- File Skipped',self::LOGKEY));
$this->msgs(self::BPM_SKIP,$this->recv->name_size_time);
// Close the file, since we are skipping it.
$this->recv->close();
break;
case self::FOP_GOT:
Log::info(sprintf('%s:- File skipped, we already have it',self::LOGKEY));
$this->msgs(self::BPM_GOTSKIP,$this->recv->name_size_time);
// Close the file, since we already have it.
$this->recv->close();
break;
case self::FOP_CONT:
Log::debug(sprintf('%s:- Continuing file [%s] from (%ld)',self::LOGKEY,$this->recv->name,$file['offs']));
case self::FOP_OK:
Log::debug(sprintf('%s:- Getting file from offset [%ld]',self::LOGKEY,$file['offs']));
//$this->msgs(self::BPM_GET,sprintf('%s %ld',$this->recv->name_size_time,($file['offs'] < 0) ? 0 : $file['offs']));
if (((int)$file['offs'] === -1) && (! $this->capGet(self::F_NOREL,self::O_THEY))) {
Log::debug(sprintf('%s:- Assuming the remote wants NR mode, since offset is [%d] and they didnt specify an OPT with it',self::LOGKEY,$file['offs']));
$this->capSet(self::F_NOREL,self::O_THEY);
}
break;
}
} catch (\Exception $e) {
Log::error(sprintf('%s:! File Open ERROR [%s]',self::LOGKEY,$e->getMessage()));
$this->msgs(self::BPM_SKIP,$this->recv->name_size_time);
// Close the file, since we had an error opening it.
if ($this->recv->fd)
$this->recv->close();
}
return TRUE;
}
/**
* @throws \Exception
*/
private function M_get(string $buf): bool
{
Log::debug(sprintf('%s:+ Sending file [%s]',self::LOGKEY,$buf));
if ($file=$this->file_parse($buf)) {
if ($this->sessionGet(self::SE_SENDFILE)
&& $this->send->sendas
&& ! strncasecmp(Arr::get($file,'file.name'),$this->send->sendas,self::MAX_PATH)
&& $this->send->mtime === Arr::get($file,'file.mtime')
&& $this->send->size === Arr::get($file,'file.size'))
{
if (! $this->send->seek($file['offs'])) {
Log::error(sprintf('%s:! Cannot send file from requested offset [%d]',self::LOGKEY,$file['offs']));
$this->msgs(self::BPM_ERR,'Can\'t send file from requested offset');
$this->send->close(FALSE);
$this->sessionClear(self::SE_SENDFILE);
} else {
$this->sessionClear(self::SE_WAITGET);
Log::debug(sprintf('%s:Sending file [%s] as [%s]',self::LOGKEY,$this->send->name,$this->send->sendas));
$this->msgs(self::BPM_FILE,sprintf('%s %lu %ld %lu %s',$this->send->sendas,$this->send->size,$this->send->mtime,$file['offs'],$file['flags']));
}
} else {
Log::error(sprintf('%s:! Remote requested an unknown file [%s]',self::LOGKEY,$buf));
}
} else {
Log::error(sprintf('%s:! UNPARSABLE file info [%s]',self::LOGKEY,$buf));
}
return TRUE;
}
/**
* M_GOT/M_SKIP commands
*
* @param string $buf
* @return bool
* @throws \Exception
*/
private function M_gotskip(string $buf): bool
{
Log::debug(sprintf('%s:+ M_gotskip [%s]',self::LOGKEY,$buf));
if ($file = $this->file_parse($buf)) {
if ($this->send->sendas
&& ! strncasecmp(Arr::get($file,'file.name'),$this->send->sendas,self::MAX_PATH)
&& $this->send->mtime === Arr::get($file,'file.mtime')
&& $this->send->size === Arr::get($file,'file.size'))
{
// @todo Commit our mail transaction if the remote end confirmed receipt of the file.
if ($this->sessionGet(self::SE_SENDFILE)) {
Log::info(sprintf('%s:= Packet/File [%s] sent.',self::LOGKEY,$this->send->name));
$this->sessionClear(self::SE_SENDFILE);
$this->send->close(TRUE);
return TRUE;
}
if ($this->sessionGet(self::SE_WAITGOT)) {
Log::info(sprintf('%s:= Packet/File [%s] sent.',self::LOGKEY,$this->send->name));
$this->sessionClear(self::SE_WAITGOT);
$this->send->close(TRUE);
} else {
Log::error(sprintf('%s:! M_got[skip] for unknown file [%s]',self::LOGKEY,$buf));
}
}
} else {
Log::error(sprintf('%s:! UNPARSABLE file info [%s]',self::LOGKEY,$buf));
}
return TRUE;
}
/**
* @throws \Exception
*/
private function M_nul(string $buf): bool
{
Log::info(sprintf('%s:+ M_NUL [%s]',self::LOGKEY,$buf));
if (! strncmp($buf,'SYS ',4)) {
$this->node->system = $this->skip_blanks(substr($buf,4));
} elseif (! strncmp($buf, 'ZYZ ',4)) {
$this->node->sysop = $this->skip_blanks(substr($buf,4));
} elseif (! strncmp($buf,'LOC ',4)) {
$this->node->location = $this->skip_blanks(substr($buf,4));
} elseif (! strncmp($buf,'NDL ',4)) {
$data = $this->skip_blanks(substr($buf,4));
$comma = strpos($data,',');
$spd = substr($data,0,$comma);
if ($comma)
$this->node->flags = substr($data,$comma+1);
if ($spd >= 300) {
$this->client->speed = $spd;
} else {
$comma = $this->skip_blanks(substr($buf,4));
$c = 0;
while (($x=substr($comma,$c,1)) && is_numeric($x))
$c++;
$comma = substr($comma,0,$c);
if (! $comma) {
$this->client->speed = self::TCP_SPEED;
} elseif (strtolower(substr($comma,$c+1,1)) === 'k') {
$this->client->speed = $spd * 1024;
} elseif (strtolower(substr($comma,$c+1,1)) === 'm') {
$this->client->speed = $spd * 1024 * 1024;
} else {
$this->client->speed = self::TCP_SPEED;
}
}
} elseif (! strncmp($buf,'TIME ',5)) {
$this->node->node_time = $this->skip_blanks(substr($buf,5));
} elseif (! strncmp($buf,'VER ',4)) {
$data = $this->skip_blanks(substr($buf,4));
$matches = [];
preg_match('#^(.+)\s+binkp/([0-9]+)\.([0-9]+)$#',$data,$matches);
$this->node->software = $matches[1];
$this->node->ver_major = $matches[2];
$this->node->ver_minor = $matches[3];
} elseif (! strncmp($buf,'TRF ',4)) {
$data = $this->skip_blanks(substr($buf,4));
$matches = [];
preg_match('/^([0-9]+)\s+([0-9]+)$/',$data,$matches);
$this->node->netmail = isset($matches[1]) ?: 0;
$this->node->files = isset($matches[2]) ?: 0;
if ($this->node->netmail + $this->node->files)
$this->sessionSet(self::SE_DELAYEOB);
} elseif (! strncmp($buf,'FREQ',4)) {
$this->sessionSet(self::SE_DELAYEOB);
} elseif (! strncmp($buf,'PHN ',4)) {
$this->node->phone = $this->skip_blanks(substr($buf,4));
} elseif (! strncmp($buf,'OPM ',4)) {
$this->node->message = $this->skip_blanks(substr($buf,4));
} elseif (! strncmp($buf,'OPT ',4)) {
$data = $this->skip_blanks(substr($buf,4));
while ($data && ($p = $this->strsep($data,' '))) {
if (! strcmp($p,'MB')) {
Log::info(sprintf('%s:- Remote wants MULTIBATCH mode',self::LOGKEY));
$this->capSet(self::F_MULTIBATCH,self::O_THEY);
} elseif (! strcmp($p,'ND')) {
Log::info(sprintf('%s:- Remote wants NO DUPES mode',self::LOGKEY));
$this->capSet(self::F_NOREL,self::O_THEY);
$this->capSet(self::F_NODUPE,self::O_THEY);
} elseif (! strcmp($p,'NDA')) {
Log::info(sprintf('%s:- Remote wants NO DUPES ASYMMETRIC mode',self::LOGKEY));
$this->capSet(self::F_NOREL, self::O_THEY);
$this->capSet(self::F_NODUPE, self::O_THEY);
$this->capSet(self::F_NODUPEA, self::O_THEY);
} elseif (! strcmp($p,'NR')) {
Log::info(sprintf('%s:- Remote wants NON RELIABLE MODE mode',self::LOGKEY));
$this->capSet(self::F_NOREL,self::O_THEY);
} elseif (! strcmp($p,'CHAT')) {
Log::info(sprintf('%s:- Remote wants CHAT mode',self::LOGKEY));
$this->capSet(self::F_CHAT,self::O_THEY);
} elseif (! strcmp($p,'CRYPT')) {
Log::info(sprintf('%s:- Remote wants CRYPT mode',self::LOGKEY));
$this->capSet(self::F_CRYPT,self::O_THEY);
} elseif (! strcmp($p,'GZ')) {
Log::info(sprintf('%s:- Remote wants GZ compression',self::LOGKEY));
$this->capSet(self::F_COMP,self::O_THEY);
} elseif (! strcmp($p,'BZ2')) {
Log::info(sprintf('%s:- Remote wants BZ2 compression',self::LOGKEY));
$this->capSet(self::F_COMP,self::O_THEY|self::O_EXT);
} elseif (! strncmp($p,'CRAM-MD5-',9) && $this->originate && $this->capGet(self::F_MD,self::O_WANT)) {
if (($x=strlen(substr($p,9))) > 64 ) {
Log::error(sprintf('%s:! The challenge string is TOO LONG [%d] (%s)',self::LOGKEY,$x,$p));
} else {
Log::info(sprintf('%s:- Remote wants MD5 auth',self::LOGKEY));
$this->md_challenge = hex2bin(substr($p,9));
if ($this->md_challenge)
$this->capSet(self::F_MD,self::O_THEY);
}
if ($this->capGet(self::F_MD,self::O_WE))
$this->capSet(self::F_MD,self::O_YES);
} else {
Log::warning(sprintf('%s:/ Ignoring UNSUPPORTED option [%s]',self::LOGKEY,$p));
}
}
} else {
Log::warning(sprintf('%s:/ M_nul Got UNKNOWN NUL [%s]',self::LOGKEY,$buf));
}
return TRUE;
}
/**
* Remote accepted our password
*
* @throws \Exception
*/
private function M_ok(string $buf): bool
{
Log::debug(sprintf('%s:+ M_ok [%s]',self::LOGKEY,$buf));
if (! $this->originate) {
Log::error(sprintf('%s:! UNEXPECTED M_OK [%s] from remote on incoming call',self::LOGKEY,$buf));
$this->rc = self::S_FAILURE;
return FALSE;
}
$buf = $this->skip_blanks($buf);
if ($this->optionGet(self::O_PWD) && $buf) {
while (($t = $this->strsep($buf," \t")))
if (strcmp($t,'non-secure') === 0) {
Log::debug(sprintf('%s:- NOT secure',self::LOGKEY));
$this->capSet(self::F_CRYPT,self::O_NO);
$this->optionClear(self::O_PWD);
break;
}
}
return $this->binkp_hsdone();
}
/**
* @throws \Exception
*/
private function M_pwd(string $buf): bool
{
$buf = $this->skip_blanks($buf);
$have_CRAM = !strncasecmp($buf,'CRAM-MD5-',9);
$have_pwd = $this->optionGet(self::O_PWD);
if ($this->originate) {
Log::error(sprintf('%s:! Unexpected password [%s] from remote on OUTGOING call',self::LOGKEY,$buf));
$this->rc = self::S_FAILURE;
return FALSE;
}
if ($this->md_challenge) {
if ($have_CRAM) {
// Loop to match passwords
$this->node->auth(substr($buf,9),$this->md_challenge);
$this->capSet(self::F_MD,self::O_THEY);
} elseif ($this->capGet(self::F_MD,self::O_NEED)) {
Log::error(sprintf('%s:! Remote doesnt support MD5, but we want it',self::LOGKEY));
$this->msgs( self::BPM_ERR,'You must support MD5 auth to talk to me');
$this->rc = self::S_FAILURE;
return FALSE;
}
}
if (! $this->md_challenge || (! $have_CRAM && (! $this->capGet(self::F_MD,self::O_NEED)))) {
// Loop to match passwords
$this->node->auth($buf);
}
if ($have_pwd) {
// If no passwords matched (ie: aka_authed is 0)
if (! $this->node->aka_authed) {
Log::error(sprintf('%s:! Bad password [%s]',self::LOGKEY,$buf));
$this->msgs(self::BPM_ERR,'Security violation');
$this->optionSet(self::O_BAD);
$this->rc = self::S_FAILURE;
return FALSE;
}
} elseif (! $this->node->aka_authed) {
Log::notice(sprintf('%s:= Remote proposed password for us [%s]',self::LOGKEY,$buf));
}
// We dont use crypt if we dont have an MD5 sessions
if (! $have_pwd && (! $this->capGet(self::F_MD,self::O_YES))) {
Log::notice(sprintf('%s:= CRYPT disabled, since we have no password or not MD5',self::LOGKEY));
$this->capSet(self::F_CRYPT,self::O_NO);
}
$opt = '';
if ($this->capGet(self::F_NOREL,self::O_WE) && $this->capGet(self::F_NODUPE,self::O_WE) && $this->capGet(self::F_NODUPEA,self::O_YES))
$opt .= ' NDA';
elseif ($this->capGet(self::F_NOREL,self::O_WE) && $this->capGet(self::F_NODUPE,self::O_WE))
$opt .= ' ND';
elseif ($this->capGet(self::F_NOREL,self::O_WE))
$opt .= ' NR';
$opt .= $this->capGet(self::F_MULTIBATCH,self::O_WE) ? ' MB' : '';
$opt .= $this->capGet(self::F_CHAT,self::O_WE) ? ' CHAT' : '';
if ($this->capGet(self::F_COMP,self::O_WE) && $this->capGet(self::F_COMP,self::O_EXT)) {
$this->comp_mode = 'BZ2';
$opt .= ' EXTCMD BZ2';
} elseif ($this->capGet(self::F_COMP,self::O_WE)) {
$this->comp_mode = 'GZ';
$opt .= ' EXTCMD GZ';
}
$opt .= $this->capGet(self::F_CRYPT,self::O_WE) ? ' CRYPT' : '';
if (strlen($opt))
$this->msgs(self::BPM_NUL,sprintf('OPT%s',$opt));
// Add our mail to the queue if we have authenticated
if ($this->node->aka_authed)
foreach ($this->node->aka_remote_authed as $ao) {
$this->send->mail($ao);
$this->send->files($ao);
}
$this->msgs(self::BPM_NUL,sprintf('TRF %lu %lu',$this->send->mail_size,$this->send->size));
$this->msgs(self::BPM_OK,sprintf('%ssecure',$have_pwd ? '' : 'non-'));
return $this->binkp_hsdone();
}
protected function protocol_init(): int
{
// Not Used
return 0;
}
/**
* Set up our BINKP session
*
* @return int
* @throws \Exception
*/
protected function protocol_session(): int
{
if ($this->binkp_init() !== self::OK)
return self::S_FAILURE;
$this->binkp_hs();
while (TRUE) {
if ($this->DEBUG)
Log::debug(sprintf('%s: - protocol_session LOOP',self::LOGKEY));
if (! $this->sessionGet(self::SE_INIT|self::SE_SENDFILE|self::SE_SENTEOB|self::SE_NOFILES) && ! $this->send->fd) {
// Open our next file to send
if ($this->send->total_count && ! $this->send->fd)
$this->send->open();
// We have an open file descriptor, set our mode to send
if ($this->send->fd) {
$this->sessionSet(self::SE_SENDFILE);
// NR mode, we wait for an M_GET before sending
if ($this->capGet(self::F_NOREL,self::O_WE)) {
$this->sessionSet(self::SE_WAITGET);
Log::debug(sprintf('%s:- NR mode, waiting for M_GET',self::LOGKEY));
}
$this->msgs(self::BPM_FILE,
sprintf('%s %lu %lu %ld %s',
$this->send->sendas,
$this->send->size,
$this->send->mtime,
$this->sessionGet(self::SE_WAITGET) ? -1 : 0,
/*$this->send->comp ?:*/ ''));
$this->sessionClear(self::SE_SENTEOB);
// We dont have anything to send
} else {
$this->sessionSet(self::SE_NOFILES);
}
}
if (! $this->sessionGet(self::SE_INIT|self::SE_WAITGOT|self::SE_SENTEOB|self::SE_DELAYEOB) && $this->sessionGet(self::SE_NOFILES)) {
$this->msgs(self::BPM_EOB,'');
$this->sessionSet(self::SE_SENTEOB);
}
$this->rc = self::S_OK;
if ($this->sessionGet(self::SE_SENTEOB) && $this->sessionGet(self::SE_RECVEOB)) {
if ($this->mib < 3 || $this->node->get_versionint() <= 100) {
break;
}
$this->mib = 0;
$this->sessionClear(self::SE_RECVEOB|self::SE_SENTEOB);
}
$wd = ($this->mqueue->count() || $this->tx_left || ($this->sessionGet(self::SE_SENDFILE) && $this->send->fd && ! $this->sessionGet(self::SE_WAITGET)));
$rd = TRUE;
try {
if ($this->DEBUG)
Log::debug(sprintf('%s: - Checking if there more data (ttySelect), timeout [%d]',self::LOGKEY,self::TIMEOUT_TIME));
// @todo we need to catch a timeout if there are no reads/writes
$rc = $this->client->ttySelect($rd,$wd,self::TIMEOUT_TIME);
if ($this->DEBUG)
Log::debug(sprintf('%s: - ttySelect returned [%d]',self::LOGKEY,$rc));
} catch (\Exception) {
$this->error_close();
$this->error = -2;
break;
}
$this->rc = ($this->originate ? (self::S_REDIAL|self::S_ADDTRY) : self::S_BUSY);
if ($rc === 0) {
$this->error_close();
$this->error = -1;
break;
}
if ($rd && ! $this->binkp_recv()) {
Log::info(sprintf('%s:- BINKP finished reading',self::LOGKEY));
break;
}
if (($this->mqueue->count() || $wd) && ! $this->binkp_send() && (! $this->send->total_count)) {
Log::info(sprintf('%s:- BINKP finished sending',self::LOGKEY));
break;
}
}
if ($this->error === -1)
Log::error(sprintf('%s:! protocol_session TIMEOUT',self::LOGKEY));
elseif ($this->error > 0)
Log::error(sprintf('%s:! During our protocol session we got ERROR [%d]',self::LOGKEY,$this->error));
while (! $this->error) {
try {
Log::info(sprintf('%s:- BINKP reading [%d]',self::LOGKEY,self::MAX_BLKSIZE));
$buf = $this->client->read(0,self::MAX_BLKSIZE);
Log::info(sprintf('%s:- BINKP got [%d] chars',self::LOGKEY,strlen($buf)));
} catch (\Exception $e) {
if ($e->getCode() !== 11) {
Log::error(sprintf('%s:! Got an exception [%d] while reading (%s)',self::LOGKEY,$e->getCode(),$e->getMessage()));
$this->error = 1;
}
break;
}
if (strlen($buf) === 0)
break;
Log::warning(sprintf('%s:- Purged [%d] bytes from input stream (%s) ',self::LOGKEY,strlen($buf),hex_dump($buf)));
}
Log::info(sprintf('%s:- We have [%d] messages and [%d] data left to send',self::LOGKEY,$this->mqueue->count(),strlen($this->tx_left)));
while (! $this->error && ($this->mqueue->count() || $this->tx_left) && $this->binkp_send()) {}
return $this->rc;
}
/**
* Strip blanks at the beginning of a string
*
* @param string $str
* @return string
* @throws \Exception
* @todo Can this be replaced with ltrim?
*/
private function skip_blanks(string $str): string
{
$c = 0;
if ($str != NULL)
while ($this->isSpace(substr($str,$c,1)))
$c++;
return substr($str,$c);
}
/**
* Return the string delimited by char and shorten the input to the remaining characters
*
* @param string $str
* @param string $char
* @return string
*/
private function strsep(string &$str,string $char): string
{
if ($x=strpos($str,$char)) {
$return = substr($str,0,$x);
$str = substr($str,$x+1);
} else {
$return = $str;
$str = '';
}
return $return;
}
/**
* Check if the string is a space
*
* @param string $str
* @return bool
* @throws \Exception
*/
private function isSpace(string $str):bool
{
if (strlen($str) > 1)
throw new \Exception('String is more than 1 char');
return $str && in_array($str,[' ',"\n","\r","\v","\f","\t"]);
}
}