clrghouz/app/Classes/Protocol/Binkp.php

1366 lines
38 KiB
PHP

<?php
namespace App\Classes\Protocol;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use League\Flysystem\UnreadableFileEncountered;
use App\Classes\Protocol as BaseProtocol;
use App\Classes\Sock\SocketClient;
use App\Classes\Sock\SocketException;
use App\Models\{Address,Setup};
final class Binkp extends BaseProtocol
{
private const LOGKEY = 'PB-';
/* -- */
private const BP_PROT = 'binkp'; /* protocol text */
private const BP_VERSION = '1.1'; /* version implemented */
private const BP_BLKSIZE = 4096; /* block size */
private const BP_TIMEOUT = 300; /* session timeout */
private const MAX_BLKSIZE = 0x7fff; /* max block size */
/* options */
private const O_NO = 0; /* I/They dont a capability? */
private const O_WANT = 1; /* I want a capability, but can be persuaded */
private const O_WE = 2; /* We agree on a capability */
private const O_THEY = 4; /* They want a capability */
private const O_NEED = 8; /* I want a capability, and wont compromise */
private const O_EXT = 16;
private const O_YES = 32;
/* messages */
private const BPM_NONE = 99; /* No available data */
private const BPM_DATA = 98; /* Binary data */
private const BPM_NUL = 0; /* Site information */
private const BPM_ADR = 1; /* List of addresses */
private const BPM_PWD = 2; /* Session password */
private const BPM_FILE = 3; /* File information */
private const BPM_OK = 4; /* Password was acknowlged (data ignored) */
private const BPM_EOB = 5; /* End Of Batch (data ignored) */
private const BPM_GOT = 6; /* File received */
private const BPM_ERR = 7; /* Misc errors */
private const BPM_BSY = 8; /* All AKAs are busy */
private const BPM_GET = 9; /* Get a file from offset */
private const BPM_SKIP = 10; /* Skip a file (RECEIVE LATER) */
private const BPM_RESERVED = 11; /* Reserved for later */
private const BPM_CHAT = 12; /* For chat */
private const BPM_MIN = self::BPM_NUL; /* Minimal message type value */
private const BPM_MAX = self::BPM_CHAT; /* Maximal message type value */
private const SE_BASE = 1;
private const SE_INIT = (1<<self::SE_BASE); /* 0000 0001 Are we in initialise mode */
private const SE_SENTEOB = (1<<self::SE_BASE+1); /* 0000 0010 Have we sent our EOB */
private const SE_RECVEOB = (1<<self::SE_BASE+2); /* 0000 0100 Have we received EOB */
private const SE_DELAYEOB = (1<<self::SE_BASE+3); /* 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_WAITGET = (1<<self::SE_BASE+4); /* 0001 0000 Wait for GET before sending a file */
private const SE_WAITGOT = (1<<self::SE_BASE+5); /* 0010 0000 We are waiting for a GOT from the remote */
private const SE_SENDFILE = (1<<self::SE_BASE+6); /* 0100 0000 Are we sending a file */
private const SE_NOFILES = (1<<self::SE_BASE+7); /* 1000 0000 We have no more files to send */
private string $md_challenge; /* The MD5 challenge with the remote system */
private int $is_msg;
private int $mib;
private int $rc;
private int $error;
private int $rx_ptr; // @todo Whats the point of this? It seems its only the size of $rx_buf?
private int $rx_size;
private string $rx_buf = '';
private ?Collection $mqueue;
private string $tx_buf;
private int $tx_ptr;
private int $tx_left;
/* BINK COMMANDS */
private const M_NUL = 0;
private const M_ADR = 1;
private const M_PWD = 2;
private const M_FILE = 3;
private const M_OK = 4;
private const M_EOB = 5;
private const M_GOTSKIP = 6;
private const M_ERR = 7;
private const M_BSY = 8;
private const M_GET = 9;
private const M_CHAT = 12;
/**
* 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()]);
$this->session(self::SESSION_BINKP,$client,(new Address));
$this->client->close();
Log::info(sprintf('%s:= onConnect - Connection closed [%s]',self::LOGKEY,$client->address_remote));
exit(0);
}
return NULL;
}
/**
* @throws Exception
*/
private function binkp_hs(): void
{
if ($this->DEBUG)
Log::debug(sprintf('%s:+ binkp_hs',self::LOGKEY));
if (! $this->originate && ($this->setup->opt_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,FALSE)));
}
$this->msgs(self::BPM_NUL,sprintf('SYS %s',$this->setup->system_name));
$this->msgs(self::BPM_NUL,sprintf('ZYZ %s',$this->setup->sysop));
$this->msgs(self::BPM_NUL,sprintf('LOC %s',$this->setup->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::BP_PROT,self::BP_VERSION));
if ($this->originate) {
$this->msgs(self::BPM_NUL,
sprintf('OPT%s%s%s%s%s%s',
($this->setup->opt_nda) ? ' NDA' : '',
($this->setup->opt_nr&self::O_WANT) ? ' NR' : '',
($this->setup->opt_nd&self::O_THEY) ? ' ND' : '',
($this->setup->opt_mb&self::O_WANT) ? ' MB' : '',
($this->setup->opt_cr&self::O_WE) ? ' CRYPT' : '',
($this->setup->opt_cht&self::O_WANT) ? ' CHAT' : ''));
}
// If we are originating, we'll show the remote our address in the same network
if ($this->originate) {
if ($this->setup->optionGet(Setup::O_HIDEAKA)) {
$addresses = collect();
foreach ($this->node->aka_remote_authed as $ao)
$addresses = $addresses->merge($this->setup->system->match($ao->zone,Address::NODE_ZC|Address::NODE_RC|Address::NODE_NC|Address::NODE_HC|Address::NODE_ACTIVE|Address::NODE_PVT|Address::NODE_POINT));
$addresses = $addresses->unique();
Log::debug(sprintf('%s: - Presenting limited AKAs [%s]',self::LOGKEY,$addresses->pluck('ftn')->join(',')));
} else {
$addresses = $this->setup->system->addresses;
Log::debug(sprintf('%s: - Presenting ALL our AKAs [%s]',self::LOGKEY,$addresses->pluck('ftn')->join(',')));
}
$this->msgs(self::BPM_ADR,$this->setup->system->addresses->pluck('ftn')->join(' '));
}
}
/**
* @return int
*/
private function binkp_hsdone(): int
{
if ($this->DEBUG)
Log::debug(sprintf('%s:+ binkp_hsdone',self::LOGKEY));
if (($this->setup->opt_nd == self::O_WE) || ($this->setup->opt_nd == self::O_THEY))
$this->setup->opt_nd = self::O_NO;
if (! $this->setup->phone)
$this->setup->phone = '-Unpublished-';
if (! $this->optionGet(self::O_PWD) || ($this->setup->opt_md != self::O_YES))
$this->setup->opt_cr = self::O_NO;
if (($this->setup->opt_cr&self::O_WE) && ($this->setup->opt_cr&self::O_THEY)) {
dump('Enable crypting messages');
/*
$this->setup->opt_cr = O_YES;
if ( bp->to ) {
init_keys( bp->keys_out, $this->node->password );
init_keys( bp->keys_in, "-" );
keys = bp->keys_in;
} else {
init_keys( bp->keys_in, $this->node->password );
init_keys( bp->keys_out, "-" );
keys = bp->keys_out;
}
for( p = $this->node->password; *p; p++ ) {
update_keys( keys, (int) *p );
}
*/
}
// @todo Implement max incoming sessions and max incoming session for the same node
// We have no mechanism to support chat
if ($this->setup->opt_cht&self::O_WANT)
Log::warning(sprintf('%s: ! We cant do chat',self::LOGKEY));
if ($this->setup->opt_nd&self::O_WE || ($this->originate && ($this->setup->opt_nr&self::O_WANT) && $this->node->get_versionint() > 100))
$this->setup->opt_nr |= self::O_WE;
if (($this->setup->opt_cht&self::O_WE) && ($this->setup->opt_cht&self::O_WANT))
$this->setup->opt_cht = self::O_YES;
$this->setup->opt_mb = (($this->node->get_versionint() > 100) || ($this->setup->opt_mb&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, MD:%d, MB:%d, CR:%d, CHT:%d',
self::LOGKEY,
$this->node->ver_major,
$this->node->ver_minor,
$this->setup->opt_nr,
$this->setup->opt_nd,
$this->setup->opt_md,
$this->setup->opt_mb,
$this->setup->opt_cr,
$this->setup->opt_cht
));
return 1;
}
private function binkp_init(): int
{
if ($this->DEBUG)
Log::debug(sprintf('%s:+ binkp_init',self::LOGKEY));
$this->sessionSet(self::SE_INIT);
$this->is_msg = -1;
$this->mib = 0;
$this->error = 0;
$this->mqueue = collect();
$this->rx_ptr = 0;
$this->rx_size = -1;
$this->tx_buf = '';
$this->tx_left = 0;
$this->tx_ptr = 0;
for ($x=0;$x<count($this->setup->binkp_options);$x++) {
switch (strtolower($this->setup->binkp_options[$x])) {
case 'p': /* Force password digest */
$this->setup->opt_md |= self::O_NEED;
case 'm': /* Want password digest */
$this->setup->opt_md |= self::O_WANT;
break;
case 'c': /* Can crypt */
$this->setup->opt_cr |= self::O_WE;
break;
case 'd': /* No dupes mode */
$this->setup->opt_nd |= self::O_NO; /*mode?O_THEY:O_NO;*/
case 'r': /* Non-reliable mode */
$this->setup->opt_nr |= ($this->originate ? self::O_WANT : self::O_NO);
break;
case 'b': /* Multi-batch mode */
$this->setup->opt_mb |= self::O_WANT;
break;
case 't': /* Chat - not implemented */
//$this->setup->opt_cht |= self::O_WANT;
break;
default:
Log::error(sprintf('%s: ! binkp_init ERROR - Unknown BINKP option [%s]',self::LOGKEY,$this->setup->binkp_options[$x]));
}
}
return self::OK;
}
/**
* @throws Exception
*/
private function binkp_recv(): int
{
if ($this->DEBUG)
Log::debug(sprintf('%s:+ binkp_recv',self::LOGKEY));
$blksz = $this->rx_size == -1 ? BinkpMessage::BLK_HDR_SIZE : $this->rx_size;
if ($this->DEBUG)
Log::debug(sprintf('%s: - binkp_recv blksize [%d] rx_size [%d].',self::LOGKEY,$blksz,$this->rx_size));
if ($blksz !== 0) {
try {
$this->rx_buf .= $this->client->read(0,$blksz-$this->rx_ptr);
} 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: - binkp_recv Socket EAGAIN',self::LOGKEY));
return 1;
}
Log::error(sprintf('%s: - binkp_recv Exception [%s].',self::LOGKEY,$e->getCode()));
$this->socket_error = $e->getMessage();
$this->error = 1;
return 0;
}
if (strlen($this->rx_buf) == 0) {
// @todo Check that this is correct.
Log::debug(sprintf('%s: - binkp_recv Was the socket closed by the remote?',self::LOGKEY));
$this->error = -2;
return 0;
}
/*
if ($this->setup->opt_cr == self::O_YES ) {
//decrypt_buf( (void *) &bp->rx_buf[bp->rx_ptr], (size_t) readsz, bp->keys_in );
}
*/
}
$this->rx_ptr = strlen($this->rx_buf);
if ($this->DEBUG)
Log::debug(sprintf('%s: - binkp_recv rx_ptr [%d] blksz [%d].',self::LOGKEY,$this->rx_ptr,$blksz));
/* Received complete block */
if ($this->rx_ptr == $blksz) {
/* Header */
if ($this->rx_size == -1 ) {
$this->is_msg = ord(substr($this->rx_buf,0,1)) >> 7;
$this->rx_size = ((ord(substr($this->rx_buf,0,1))&0x7f) << 8 )+ord(substr($this->rx_buf,1,1));
$this->rx_ptr = 0;
if ($this->DEBUG)
Log::debug(sprintf('%s: - binkp_recv HEADER, is_msg [%d], rx_size [%d]',self::LOGKEY,$this->is_msg,$this->rx_size));
if ($this->rx_size == 0)
goto ZeroLen;
$rc = 1;
/* Next block */
} else {
ZeroLen:
if ($this->DEBUG)
Log::debug(sprintf('%s: - binkp_recv NEXT BLOCK, is_msg [%d]',self::LOGKEY,$this->is_msg));
if ($this->is_msg) {
$this->mib++;
/* Handle zero length block */
if ($this->rx_size == 0 ) {
Log::debug(sprintf('%s:- binkp_recv Zero length msg - dropped',self::LOGKEY));
$this->rx_size = -1;
$this->rx_ptr = 0;
$this->rx_buf = '';
return 1;
}
if ($this->DEBUG)
Log::debug(sprintf('%s: - binkp_recv BUFFER [%d]',self::LOGKEY,strlen($this->rx_buf)));
$rc = ord(substr($this->rx_buf,0,1));
if ($rc > self::BPM_MAX) {
Log::error(sprintf('%s: ! binkp_recv Unknown Message [%s] (%d)',self::LOGKEY,$this->rx_buf,strlen($this->rx_buf)));
$rc = 1;
} else {
//DEBUG(('B',2,"rcvd %s '%s'%s",mess[rc],bp->rx_buf + 1,CRYPT(bps))); //@todo CRYPT
$data = substr($this->rx_buf,1);
switch ($rc) {
case self::M_ADR:
$rc = $this->M_adr($data);
break;
case self::M_EOB:
$rc = $this->M_eob($data);
break;
case self::M_NUL:
$rc = $this->M_nul($data);
break;
case self::M_PWD:
$rc = $this->M_pwd($data);
break;
case self::M_ERR:
$rc = $this->M_err($data);
break;
case self::M_FILE:
$rc = $this->M_file($data);
break;
case self::M_GET:
$rc = $this->M_get($data);
break;
case self::M_GOTSKIP:
$rc = $this->M_gotskip($data);
break;
case self::M_OK:
$rc = $this->M_ok($data);
break;
case self::M_CHAT:
$rc = $this->M_chat($data);
break;
default:
Log::error(sprintf('%s: ! binkp_recv Command not implemented [%d]',self::LOGKEY,$rc));
$rc = 1;
}
}
} else {
if ($this->recv->fd) {
try {
$rc = $this->recv->write($this->rx_buf);
} 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 = 1;
}
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_GOT,$this->recv->name_size_time);
$this->recv->close();
$rc = 1;
}
} else {
Log::critical(sprintf('%s: - binkp_recv Ignoring data block', self::LOGKEY));
$rc = 1;
}
}
$this->rx_ptr = 0;
$this->rx_size = -1;
}
$this->rx_buf = '';
} else {
$rc = 1;
}
if ($this->DEBUG)
Log::debug(sprintf('%s:= binkp_recv [%d]',self::LOGKEY,$rc));
return $rc;
}
/**
* @throws Exception
*/
private function binkp_send(): int
{
if ($this->DEBUG)
Log::debug(sprintf('%s:+ binkp_send - tx_left [%d]',self::LOGKEY,$this->tx_left));
if ($this->tx_left == 0 ) { /* tx buffer is empty */
$this->tx_ptr = $this->tx_left = 0;
if ($this->DEBUG)
Log::debug(sprintf('%s: - binkp_send msgs [%d]',self::LOGKEY,$this->mqueue->count()));
if ($this->mqueue->count()) { /* there are unsent messages */
while ($msg = $this->mqueue->shift()) {
if (($msg->len+$this->tx_left) > self::MAX_BLKSIZE) {
break;
}
$this->tx_buf .= $msg->msg;
$this->tx_left += $msg->len;
}
} elseif ($this->sessionGet(self::SE_SENDFILE) && $this->send->fd && (! $this->sessionGet(self::SE_WAITGET))) {
$data = '';
try {
$data = $this->send->read(self::BP_BLKSIZE);
} 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 ($data) {
$this->tx_buf .= BinkpMessage::mkheader(strlen($data));
$this->tx_buf .= $data;
/*
if ($this->setup->opt_cr == self::O_YES) {
encrypt_buf($this->tx_buf,($data + BinkpMessage::BLK_HDR_SIZE),$this->keys_out);
}
*/
$this->tx_left = strlen($data)+BinkpMessage::BLK_HDR_SIZE;
}
// @todo should this be less than BP_BLKSIZE? Since a read could return a blocksize and it could be the end of the file?
if ($data < self::BP_BLKSIZE && $this->send->filepos == $this->send->size) {
$this->sessionSet(self::SE_WAITGOT);
$this->sessionClear(self::SE_SENDFILE);
}
}
} else {
try {
$rc = $this->client->send(substr($this->tx_buf,$this->tx_ptr,$this->tx_left),self::BP_TIMEOUT);
} catch (Exception $e) {
if ($e->getCode() == 11)
return 1;
$this->socket_error = $e->getMessage();
Log::error(sprintf('%s:! binkp_send - ERROR [%s]',self::LOGKEY,$e->getMessage()));
return 0;
}
$this->tx_ptr += $rc;
$this->tx_left -= $rc;
if (! $this->tx_left) {
$this->tx_buf = '';
$this->tx_ptr = 0;
}
}
if ($this->DEBUG)
Log::debug(sprintf('%s:= binkp_send [1]',self::LOGKEY));
return 1;
}
private function file_parse(string $str): ?array
{
if ($this->DEBUG)
Log::debug(sprintf('%s:+ file_parse [%s]',self::LOGKEY,$str));
$name = $this->strsep($str,' ');
$size = $this->strsep($str,' ');
$time = $this->strsep($str,' ');
$offs = $this->strsep($str,' ');
if ($name && $size && $time) {
return [
'file'=>['name'=>$name,'size'=>$size,'mtime'=>$time],
'offs'=>$offs
];
}
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
{
if ($this->DEBUG)
Log::debug(sprintf('%s:+ msgs [%d:%s]',self::LOGKEY,$id,$msg_body));
$this->mqueue->push(new BinkpMessage($id,$msg_body));
/*
if ($this->setup->opt_cr == self::O_YES) {
//$this->encrypt_buf($this->bps->mqueue[$this->nmsgs]->msg,$this->bps->mqueue[$this->nmsgs]->len,$this->bps->keys_out);
}
*/
$this->mib++;
}
/**
* @throws Exception
*/
private function M_adr(string $buf): int
{
Log::debug(sprintf('%s:+ M_adr [%s]',self::LOGKEY,$buf));
$buf = $this->skip_blanks($buf);
$rc = 0;
while(($rem_aka = $this->strsep($buf,' '))) {
Log::info(sprintf('%s: - Parsing AKA [%s]',self::LOGKEY,$rem_aka));
try {
if (! ($o = Address::findFTN($rem_aka))) {
Log::debug(sprintf('%s: ? AKA is UNKNOWN [%s]',self::LOGKEY,$rem_aka));
continue;
}
} 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 0;
}
// Check if the remote has our AKA
if ($this->setup->system->addresses->pluck('ftn')->search($rem_aka) !== FALSE) {
Log::error(sprintf('%s: ! AKA is OURS [%s]',self::LOGKEY,$rem_aka));
$this->msgs(self::BPM_ERR,sprintf('Sorry that is my AKA [%s]',$rem_aka));
$this->rc = self::S_FAILURE;
return 0;
}
// @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');
$this->rc = ($this->originate ? self::S_REDIAL|self::S_ADDTRY : self::S_BUSY);
return 0;
}
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|self::S_ADDTRY;
return 0;
}
if ($this->md_challenge) {
$this->msgs(self::BPM_PWD,sprintf('CRAM-MD5-%s',$this->node->get_md5chal($this->md_challenge)));
} elseif ($this->setup->opt_md == self::O_YES ) {
$this->msgs(self::BPM_ERR,'Can\'t use plaintext password');
$this->rc = self::S_FAILURE|self::S_ADDTRY;
return 0;
} else {
$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 1;
}
private function M_chat(string $buf): int
{
Log::debug(sprintf('%s:+ M_chat [%s]',self::LOGKEY,$buf));
if ($this->setup->opt_cht == 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',self::LOGKEY));
}
return 1;
}
/**
* We received EOB from the remote.
*
* @throws Exception
*/
private function M_eob(string $buf): int
{
Log::debug(sprintf('%s:+ M_eob [%s]',self::LOGKEY,$buf));
if ($this->recv->fd)
$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: - M_eob Checking for any new mail to [%s]',self::LOGKEY,$ao->ftn));
$this->send->mail($ao);
}
if ($this->send->total_count)
$this->sessionClear(self::SE_NOFILES|self::SE_SENTEOB);
}
return 1;
}
/**
* @throws Exception
*/
private function M_err(string $buf): int
{
Log::debug(sprintf('%s:+ M_err [%s]',self::LOGKEY,$buf));
$this->error_close();
$this->rc = ($this->originate ? (self::S_REDIAL|self::S_ADDTRY) : self::S_BUSY);
return 0;
}
/**
* @throws Exception
*/
private function M_file(string $buf): int
{
Log::debug(sprintf('%s:+ M_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"',$buf));
if ($this->sessionGet(self::SE_SENDFILE))
$this->send->close(FALSE);
$this->rc = ($this->originate ? self::S_REDIAL|self::S_ADDTRY : self::S_BUSY);
return 0;
}
// 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);
return 1;
}
$this->recv->new($file['file']);
try {
switch ($this->recv->open($this->node->address,$file['offs']<0)) {
case self::FOP_ERROR:
Log::error(sprintf('%s: ! File ERROR',self::LOGKEY));
case self::FOP_SUSPEND:
Log::info(sprintf('%s: - File Suspended',self::LOGKEY));
$this->msgs(self::BPM_SKIP,$this->recv->name_size_time);
break;
case self::FOP_SKIP:
Log::info(sprintf('%s: - File Skipped',self::LOGKEY));
$this->msgs(self::BPM_GOT,$this->recv->name_size_time);
break;
case self::FOP_OK:
Log::debug(sprintf('%s: - Getting file from [%d]',self::LOGKEY,$file['offs']));
if ($file['offs'] != -1) {
if (! ($this->setup->opt_nr&self::O_THEY)) {
$this->setup->opt_nr |= self::O_THEY;
}
break;
}
case self::FOP_CONT:
Log::debug(sprintf('%s: - Continuing file [%s] (%ld)',self::LOGKEY,$this->recv->name,$file['offs']));
$this->msgs(self::BPM_GET,sprintf('%s %ld',$this->recv->name_size_time,($file['offs'] < 0) ? 0 : $file['offs']));
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 1;
}
/**
* @throws Exception
*/
private function M_get(string $buf): int
{
Log::debug(sprintf('%s:+ M_get [%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 packet [%s] as [%s]',self::LOGKEY,$this->send->name,$this->send->sendas));
$this->msgs(self::BPM_FILE,sprintf('%s %lu %ld %lu',$this->send->sendas,$this->send->size,$this->send->mtime,$file['offs']));
}
} else {
Log::error(sprintf('%s: ! M_got for unknown file [%s]',self::LOGKEY,$buf));
}
} else {
Log::error(sprintf('%s: - UNPARSABLE file info [%s]',self::LOGKEY,$buf));
}
return 1;
}
/**
* M_GOT/M_SKIP commands
*
* @param string $buf
* @return int
* @throws Exception
*/
private function M_gotskip(string $buf):int
{
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::debug(sprintf('%s:Packet [%s] sent. (%s)',self::LOGKEY,$this->send->sendas,$this->send->name));
$this->sessionClear(self::SE_SENDFILE);
$this->send->close(TRUE);
return 1;
}
if ($this->sessionGet(self::SE_WAITGOT)) {
Log::debug(sprintf('%s:Packet [%s] sent. (%s)',self::LOGKEY,$this->send->sendas,$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 1;
}
/**
* @throws Exception
*/
private function M_nul(string $buf): int
{
Log::debug(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 = SocketClient::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 = SocketClient::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,'NR')) {
$this->setup->opt_nr |= self::O_WE;
} elseif (! strcmp($p,'MB')) {
$this->setup->opt_mb |= self::O_WE;
} elseif (! strcmp($p,'ND')) {
$this->setup->opt_nd |= self::O_WE;
} elseif (! strcmp($p,'NDA')) {
$this->setup->opt_nd |= self::O_EXT;
} elseif (! strcmp($p,'CHAT')) {
$this->setup->opt_cht |= self::O_WE;
} elseif (! strcmp($p,'CRYPT')) {
$this->setup->opt_cr |= self::O_THEY;
} elseif (! strncmp($p,'CRAM-MD5-',9) && $this->originate && $this->setup->opt_md) {
if (($x=strlen(substr($p,9))) > 64 ) {
Log::error(sprintf('%s: - M_nul Got TOO LONG [%d] challenge string',self::LOGKEY,$x));
} else {
$this->md_challenge = hex2bin(substr($p,9));
if ($this->md_challenge)
$this->setup->opt_md |= self::O_THEY;
}
if (($this->setup->opt_md&(self::O_THEY|self::O_WANT)) == (self::O_THEY|self::O_WANT))
$this->setup->opt_md = self::O_YES;
} else { /* if ( strcmp( p, "GZ" ) || strcmp( p, "BZ2" ) || strcmp( p, "EXTCMD" )) */
Log::warning(sprintf('%s: - M_nul Got UNSUPPORTED option [%s]',self::LOGKEY,$p));
}
$data = substr($data,strpos($data,' '));
}
} else {
Log::warning(sprintf('%s: - M_nul Got UNKNOWN NUL [%s]',self::LOGKEY,$buf));
}
return 1;
}
/**
* Remote accepted our password
*
* @throws Exception
*/
private function M_ok(string $buf): int
{
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 0;
}
$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: - Non Secure',self::LOGKEY));
$this->setup->opt_cr = self::O_NO;
$this->optionClear(self::O_PWD);
break;
}
}
// 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->msgs(self::BPM_NUL,sprintf('TRF %lu %lu',$this->send->mail_size,$this->send->file_size));
Log::debug(sprintf('%s:= M_ok',self::LOGKEY));
return $this->binkp_hsdone();
}
/**
* @throws Exception
*/
private function M_pwd(string $buf): int
{
Log::debug(sprintf('%s:+ M_pwd [%s]',self::LOGKEY,$buf));
$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 0;
}
if ($this->md_challenge) {
if ($have_CRAM) {
// Loop to match passwords
$this->node->auth(substr($buf,9),$this->md_challenge);
$this->setup->opt_md |= self::O_THEY;
} elseif ($this->setup->opt_md&self::O_NEED) {
Log::error(sprintf('%s: ! Remote doesnt support MD5',self::LOGKEY));
$this->msgs( self::BPM_ERR,'You must support MD5');
$this->rc = self::S_FAILURE;
return 0;
}
}
if (! $this->md_challenge || (! $have_CRAM && ! ($this->setup->opt_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 0;
}
} elseif (! $this->node->aka_authed) {
Log::notice(sprintf('%s: - Remote proposed password for us [%s]',self::LOGKEY,$buf));
}
if (($this->setup->opt_md&(self::O_THEY|self::O_WANT )) == (self::O_THEY|self::O_WANT))
$this->setup->opt_md = self::O_YES;
if (!$have_pwd || $this->setup->opt_md != self::O_YES)
$this->setup->opt_cr = self::O_NO;
$tmp = sprintf('%s%s%s%s%s%s',
($this->setup->opt_nr&self::O_WANT) ? ' NR' : '',
($this->setup->opt_nd&self::O_THEY) ? ' ND' : '',
($this->setup->opt_mb&self::O_WANT) ? ' MB' : '',
($this->setup->opt_cht&self::O_WANT) ? ' CHAT' : '',
(! ($this->setup->opt_nd&self::O_WE) != (! ($this->setup->opt_nd&self::O_THEY))) ? ' NDA': '',
(($this->setup->opt_cr&self::O_WE) && ($this->setup->opt_cr&self::O_THEY )) ? ' CRYPT' : '');
if (strlen($tmp))
$this->msgs(self::BPM_NUL,sprintf('OPT%s',$tmp));
// 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->msgs(self::BPM_NUL,sprintf('TRF %lu %lu',$this->send->mail_size,$this->send->file_size));
$this->msgs(self::BPM_OK,sprintf('%ssecure',$have_pwd ? '' : 'non-'));
return $this->binkp_hsdone();
}
protected function protocol_init(): int
{
// Not Used
return 0;
}
/**
* Setup our BINKP session
*
* @return int
* @throws Exception
*/
protected function protocol_session(): int
{
Log::debug(sprintf('%s:+ protocol_session',self::LOGKEY));
if ($this->binkp_init() != self::OK)
return $this->originate ? (self::S_REDIAL|self::S_ADDTRY) : self::S_FAILURE;
$this->binkp_hs();
while (TRUE) {
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();
if ($this->send->fd) {
$this->sessionSet(self::SE_SENDFILE);
if ($this->setup->opt_nr&self::O_WE) {
$this->sessionSet(self::SE_WAITGET);
Log::debug(sprintf('%s: - Waiting for M_GET',self::LOGKEY));
}
$this->msgs(self::BPM_FILE,
sprintf('%s %lu %lu %ld',$this->send->sendas,$this->send->size,$this->send->mtime,$this->sessionGet(self::SE_WAITGET) ? -1 : 0));
$this->sessionClear(self::SE_SENTEOB);
} 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 {
// @todo we need to catch a timeout if there are no reads/writes
$rc = $this->client->ttySelect($rd,$wd,self::BP_TIMEOUT);
} 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())
break;
if ($this->DEBUG)
Log::debug(sprintf('%s: - mqueue [%d]',self::LOGKEY,$this->mqueue->count()));
if (($this->mqueue->count() || $wd) && ! $this->binkp_send() && (! $this->send->total_count))
break;
}
if ($this->error == -1)
Log::error(sprintf('%s: ! protocol_session TIMEOUT',self::LOGKEY));
elseif ($this->error > 0)
Log::error(sprintf('%s: ! protocol_session Got ERROR [%d]',self::LOGKEY,$this->socket_error));
while (! $this->error) {
try {
$buf = $this->client->read(0,self::MAX_BLKSIZE);
} catch (Exception $e) {
if ($e->getCode() !== 11) {
Log::debug(sprintf('%s: ? protocol_session Got Exception [%d] (%s)',self::LOGKEY,$e->getCode(),$e->getMessage()));
$this->error = 1;
}
break;
}
if (strlen($buf) == 0)
break;
Log::warning(sprintf('%s: - Purged (%s) [%d] bytes from input stream',self::LOGKEY,hex_dump($buf),strlen($buf)));
}
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
*/
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
{
$return = strstr($str,$char,TRUE) ?: $str;
$str = substr($str,strlen($return)+strlen($char));
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"]);
}
}