Compare commits

2 Commits
master ... bbs

Author SHA1 Message Date
2dd7a6ebd3 Move ANSI* commands to BBS/
All checks were successful
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 34s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 1m41s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
2024-05-28 12:46:06 +10:00
c9688ef373 BBS ported from vbbs 2024-05-28 12:44:55 +10:00
336 changed files with 12117 additions and 11363 deletions

View File

@@ -1,25 +1,55 @@
APP_NAME="Clearing Houz"
APP_ENV=production
APP_KEY=
APP_MAINTENANCE_DRIVER=cache
APP_MAINTENANCE_STORE=memcached
APP_DEBUG=false
APP_URL=http://clrghouz
APP_TIMEZONE=
APP_URL=
AUTH_PASSWORD_RESET_TOKEN_TABLE=password_resets
CACHE_STORE=memcached
MEMCACHED_HOST=memcached
LOG_CHANNEL=stack
LOG_LEVEL=info
DB_CONNECTION=pgsql
DB_HOST=postgres
DB_PORT=5432
DB_DATABASE=clrghouz
DB_USERNAME=clrghouz
DB_PASSWORD=
#DB_SSLMODE=prefer
#DB_SSLROOTCERT=/var/www/html/config/ssl/ca.crt
#DB_SSLCERT=/var/www/html/config/ssl/client.crt
#DB_SSLKEY=/var/www/html/config/ssl/client.key
BROADCAST_DRIVER=log
MEMCACHED_HOST=memcached
CACHE_DRIVER=memcached
QUEUE_CONNECTION=database
SESSION_DRIVER=file
SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=
REDIS_PORT=6379
MAIL_DRIVER=smtp
MAIL_HOST=mail.dege.lan
MAIL_PORT=25
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=
MAIL_AUTO_EMBED_METHOD=base64
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
FIDO_DIR=fido
FIDO_PACKET_KEEP=
FIDO_STRICT=false
FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
@@ -27,24 +57,3 @@ AWS_BUCKET=
AWS_ENDPOINT=
AWS_DEFAULT_REGION=home
AWS_USE_PATH_STYLE_ENDPOINT=true
LOG_CHANNEL=daily
LOG_LEVEL=info
LOG_DAILY_DAYS=93
MAIL_MAILER=smtp
MAIL_HOST=smtp
MAIL_PORT=25
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=
MAIL_AUTO_EMBED_METHOD=base64
SESSION_DRIVER=file
# Clrghouz configuration
FIDO_DNS_NS=
MATRIX_SERVER=
MATRIX_AS_TOKEN=
MATRIX_HS_TOKEN=

View File

@@ -5,8 +5,6 @@ APP_DEBUG=true
APP_URL=http://clrghouz
APP_TIMEZONE=Australia/Melbourne
CACHE_STORE=array
LOG_CHANNEL=stderr
LOG_LEVEL=debug
@@ -15,7 +13,7 @@ DB_HOST=postgres-test
DB_PORT=5432
DB_DATABASE=test
DB_USERNAME=test
DB_PASSWORD=password
DB_PASSWORD=test
BROADCAST_DRIVER=log
CACHE_DRIVER=file

View File

@@ -74,7 +74,8 @@ jobs:
( dockerd --host=tcp://0.0.0.0:2375 --tls=false & ) && sleep 3
## Some debugging info
# docker info && docker version
# env|sort
env|sort
echo "PRT: ${{ secrets.PKG_WRITE_TOKEN }}"
- name: Registry FQDN Setup
id: registry
@@ -92,10 +93,12 @@ jobs:
- name: Code Checkout
uses: actions/checkout@v4
- name: Record version and Delete Unnecessary files
- name: Record version
run: |
pwd
ls -al
echo ${GITHUB_SHA::8} > VERSION
rm -rf .git* tests/ storage/app/test/
cat VERSION
- name: Build and Push Docker Image
uses: docker/build-push-action@v5

View File

@@ -1,9 +1,8 @@
<?php
namespace App\Models\Casts;
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
class CollectionOrNull implements CastsAttributes
@@ -11,13 +10,13 @@ class CollectionOrNull implements CastsAttributes
/**
* Cast the given value.
*
* @param Model $model
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return Collection
*/
public function get(Model $model,string $key,$value,array $attributes): Collection
public function get($model,string $key,$value,array $attributes): Collection
{
return collect(json_decode($value, true));
}
@@ -31,7 +30,7 @@ class CollectionOrNull implements CastsAttributes
* @param array $attributes
* @return string|null
*/
public function set(Model $model,string $key,$value,array $attributes): ?string
public function set($model,string $key,$value,array $attributes): ?string
{
return ($value->count()) ? json_encode($value) : NULL;
}

View File

@@ -1,23 +1,24 @@
<?php
namespace App\Models\Casts;
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class CompressedStringOrNull implements CastsAttributes
class CompressedString implements CastsAttributes
{
/**
* Cast the given value.
*
* For postgresl bytea columns the value is a resource stream
*
* @param Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return string|null
* @note postgres bytea columns the value is a resource stream
* @return string
*/
public function get(Model $model,string $key,mixed $value,array $attributes): ?string
public function get($model,string $key,mixed $value,array $attributes): string
{
// For stream resources, we to fseek in case we've already read it.
if (is_resource($value))
@@ -27,7 +28,13 @@ class CompressedStringOrNull implements CastsAttributes
? stream_get_contents($value)
: $value;
return $value ? zstd_uncompress(base64_decode($value)) : NULL;
// If we get an error decompressing, it might not be zstd (or its already been done)
try {
return $value ? zstd_uncompress(base64_decode($value)) : '';
} catch (\ErrorException $e) {
return $value;
}
}
/**
@@ -37,10 +44,10 @@ class CompressedStringOrNull implements CastsAttributes
* @param string $key
* @param mixed $value
* @param array $attributes
* @return string|null
* @return string
*/
public function set(Model $model,string $key,$value,array $attributes): ?string
public function set($model,string $key,$value,array $attributes): string
{
return $value ? base64_encode(zstd_compress($value)) : NULL;
return $value ? base64_encode(zstd_compress($value)) : '';
}
}

101
app/Classes/BBS/Control.php Normal file
View File

@@ -0,0 +1,101 @@
<?php
namespace App\Classes\BBS;
use App\Classes\BBS\Control\EditFrame;
use App\Classes\BBS\Control\Register;
use App\Classes\BBS\Control\Telnet;
abstract class Control
{
const prefix = 'App\Classes\Control\\';
// Has this control class finished with input
protected bool $complete = FALSE;
// The server object that is running this control class
protected Server $so;
/**
* What is the state of the server outside of this control.
* Should only contain
* + mode = Mode to follow outside of the control method
* + action = Action to run after leaving the control method
*
* @var array
*/
public array $state = [];
abstract public function handle(string $read): string;
public static function factory(string $name,Server $so,array $args=[])
{
switch ($name) {
case 'editframe':
return new EditFrame($so,$args);
case 'register':
return new Register($so);
case 'telnet':
return new Telnet($so);
default:
$c = (class_exists($name)) ? $name : self::prefix.$name;
$o = class_exists($c) ? new $c($so,$args) : NULL;
$so->log('debug',sprintf(($o ? 'Executing: %s' : 'Class doesnt exist: %s'),$c));
return $o;
}
}
public function __construct(Server $so,array $args=[])
{
$this->so = $so;
// Boot control, preparing anything before keyboard entry
$this->boot();
$this->so->log('info',sprintf('Initialised control %s',get_class($this)));
}
public function __get(string $key): mixed
{
switch ($key) {
case 'complete':
return $this->complete;
case 'name':
return get_class($this);
default:
throw new \Exception(sprintf('%s:! Unknown key: %s',static::LOGKEY,$key));
}
}
// Default boot method if a child class doesnt have one.
protected function boot()
{
$this->state['mode'] = FALSE;
}
/**
* Has control completed?
* @deprecated use $this->complete;
*/
public function complete()
{
return $this->complete;
}
/**
* If completing an Action frame, this will be called to submit the data.
*
* Ideally this should be overridden in a child class.
*/
public function process()
{
$this->complete = TRUE;
}
}

View File

@@ -0,0 +1,198 @@
<?php
namespace App\Classes\BBS\Control;
use Illuminate\Support\Arr;
use App\Classes\BBS\Control;
use App\Classes\BBS\Frame;
use App\Classes\BBS\Server;
/**
* Class Edit Frame handles frame editing
*
* @package App\Classes\Control
*/
class EditFrame extends Control
{
private $x = 1;
private $y = 1;
// The frame applicable for this control (not the current rendered frame, thats in $so)
protected $fo = NULL;
public function __construct(Server $so,array $args=[])
{
if (! $args OR ! Arr::get($args,'fo') OR (! $args['fo'] instanceof Frame))
throw new \Exception('Missing frame to Edit');
$this->fo = $args['fo'];
parent::__construct($so);
}
protected function boot()
{
// Clear screen and setup edit.
$this->so->co->send(CLS.HOME.DOWN.CON);
// @todo Add page number + "EDIT" (prob only required for login pages which dont show page num)
$this->so->co->send($this->fo->raw().$this->so->moveCursor(1,2));
$this->updateBaseline();
}
public function handle(string $read): string
{
static $esc = FALSE;
static $brace = FALSE;
static $out = '';
static $key = '';
$out .= $read;
switch ($read)
{
case 'A':
if ($esc AND $brace)
{
$this->y--;
if ($this->y < 1) {
$this->y = 1;
$out = '';
}
$brace = $esc = FALSE;
}
break;
case 'B':
if ($esc AND $brace)
{
$this->y++;
if ($this->y > $this->fo->frame_length()) {
$this->y = $this->fo->frame_length();
$out = '';
}
$brace =$esc = FALSE;
}
break;
case 'C':
if ($esc AND $brace)
{
$this->x++;
if ($this->x > $this->fo->frame_width()) {
$this->x = $this->fo->frame_width();
$out = '';
}
$brace =$esc = FALSE;
}
break;
case 'D':
if ($esc AND $brace)
{
$this->x--;
if ($this->x < 1) {
$this->x = 1;
$out = '';
}
$brace = $esc = FALSE;
}
break;
case '[':
if ($esc)
$brace = TRUE;
break;
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case '0':
if ($esc AND $brace) {
$key .= $read;
} else {
$this->x++;
}
break;
case '~':
if ($esc AND $brace)
{
switch ($key)
{
// F9 Pressed
case 20:
break;
// F10 Pressed
case 21:
$this->complete = TRUE;
$this->state = ['action'=>ACTION_GOTO,'mode'=>NULL];
break;
}
$brace = $esc = FALSE;
$key = '';
}
break;
case ESC;
$esc = TRUE;
break;
case LF: $this->y++; break;
case CR; $this->x = 1; break;
default:
if ($esc)
$esc = FALSE;
$this->x++;
}
if (! $esc)
{
printf(" . SENDING OUT: %s\n",$out);
$this->so->co->send($out);
$this->updateBaseline();
$out = '';
}
printf(" . X:%d,Y:%d,C:%s,ESC:%s\n",
$this->x,
$this->y,
(ord($read) < 32 ? '.' : $read),
($esc AND $brace) ? 'TRUE' : 'FALSE');
return $read;
}
public function updateBaseline()
{
$this->so->sendBaseline(
$this->so->co,
sprintf('%02.0f:%02.0f]%s'.RESET.'[',
$this->y,
$this->x,
($this->fo->attr($this->x,$this->y) != '-' ? ESC.'['.$this->fo->attr($this->x,$this->y) : '').$this->fo->char($this->x,$this->y),
)
);
}
public function process()
{
dump(__METHOD__);
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace App\Classes\BBS\Control;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Validator;
use App\Classes\BBS\Control;
use App\Mail\SendToken;
use App\Models\User;
/**
* Class Register handles registration
*
* @todo REMOVE the force .WHITE at the end of each sendBaseline()
* @package App\Classes\Control
*/
class Register extends Control
{
private $data = [];
protected function boot()
{
$this->so->sendBaseline($this->so->co,GREEN.'Select User Name'.WHITE);
}
/**
* Handle Registration Form Input
*
* This function assumes the form has 7 fields in a specific order.
*
* @todo Make this form more dynamic, or put some configuration in a config file, so that there is flexibility
* in field placement.
* @param string $read
* @param array $current
* @return string
*/
public function handle(string $read,array $current=[]): string
{
// Ignore LF (as a result of pressing ENTER)
if ($read == LF)
return '';
// If we got a # we'll be completing field input.
if ($read == HASH OR $read == CR) {
// Does our field have data...
if ($x=$this->so->fo->getFieldCurrentInput()) {
switch ($this->so->fo->getFieldId()) {
// Username
case 0:
// See if the requested username already exists
if (User::where('login',$x)->exists()) {
$this->so->sendBaseline($this->so->co,RED.'USER ALREADY EXISTS'.WHITE);
return '';
}
$this->so->sendBaseline($this->so->co,GREEN.'Enter Real Name'.WHITE);
break;
// Real Name
case 1:
//$this->data['name'] = $x;
$this->so->sendBaseline($this->so->co,GREEN.'Enter Email Address'.WHITE);
break;
// Email Address
case 2:
if (Validator::make(['email'=>$x],[
'email'=>'email',
])->fails()) {
$this->so->sendBaseline($this->so->co,RED.'INVALID EMAIL ADDRESS'.WHITE);
return '';
};
// See if the requested email already exists
if (User::where('email',$x)->exists()) {
$this->so->sendBaseline($this->so->co,RED.'USER ALREADY EXISTS'.WHITE);
return '';
}
$this->data['email'] = $x;
$this->data['token'] = sprintf('%06.0f',rand(0,999999));
$this->so->sendBaseline($this->so->co,YELLOW.'PROCESSING...'.WHITE);
Mail::to($this->data['email'])->sendNow(new SendToken($this->data['token']));
if (Mail::failures()) {
dump('Failure?');
dump(Mail::failures());
}
$this->so->sendBaseline($this->so->co,GREEN.'Enter Password'.WHITE);
break;
// Enter Password
case 3:
$this->data['password'] = $x;
$this->so->sendBaseline($this->so->co,GREEN.'Confirm Password'.WHITE);
break;
// Confirm Password
case 4:
if ($this->data['password'] !== $x) {
$this->so->sendBaseline($this->so->co,RED.'PASSWORD DOESNT MATCH, *09 TO START AGAIN'.WHITE);
return '';
}
$this->so->sendBaseline($this->so->co,GREEN.'Enter Location'.WHITE);
break;
// Enter Location
case 5:
$this->so->sendBaseline($this->so->co,GREEN.'Enter TOKEN emailed to you'.WHITE);
break;
// Enter Token
case 6:
if ($this->data['token'] !== $x) {
$this->so->sendBaseline($this->so->co,RED.'TOKEN DOESNT MATCH, *09 TO START AGAIN'.WHITE);
return '';
}
$this->complete = TRUE;
break;
default:
$this->so->sendBaseline($this->so->co,RED.'HUH?');
}
} else {
// If we are MODE_BL, we need to return the HASH, otherwise nothing.
if (in_array($this->state['mode'],[MODE_BL,MODE_SUBMITRF,MODE_RFNOTSENT])) {
return $read;
} else {
$this->so->sendBaseline($this->so->co,RED.'FIELD REQUIRED...'.WHITE);
return '';
}
}
}
return $read;
}
}

View File

@@ -0,0 +1,199 @@
<?php
namespace App\Classes\BBS\Control;
use App\Classes\BBS\Control;
/**
* Class Telnet
*
* This class looks after any telnet session commands
*
* TELNET http://pcmicro.com/netfoss/telnet.html
*
* @package App\Classes\Control
*/
final class Telnet extends Control
{
protected const LOGKEY = 'CT-';
/** @var int Data Byte */
public const TCP_IAC = 0xff;
/** @var int Indicates the demand that the other party stop performing, or confirmation that you are no
longer expecting the other party to perform, the indicated option */
public const TCP_DONT = 0xfe;
/** @var int Indicates the request that the other party perform, or confirmation that you are expecting
the other party to perform, the indicated option. */
public const TCP_DO = 0xfd;
/** @var int Indicates the refusal to perform, or continue performing, the indicated option. */
public const TCP_WONT = 0xfc;
/** @var int Indicates the desire to begin performing, or confirmation that you are now performing, the indicated option. */
public const TCP_WILL = 0xfb;
/** @var int Indicates that what follows is sub-negotiation of the indicated option. */
public const TCP_SB = 0xfa;
/** @var int The GA signal. */
public const TCP_GA = 0xf9;
/** @var int Erase Line. */
public const TCP_EL = 0xf8;
/** @var int Erase character. */
public const TCP_EC = 0xf7;
/** @var int Are you there? */
public const TCP_AYT = 0xf6;
/** @var int About output */
public const TCP_AO = 0xf5;
/** @var int Interrupt Process. */
public const TCP_IP = 0xf4;
/** @var int Break. */
public const TCP_BREAK = 0xf3;
/** @var int The data stream portion of a Synch. This should always be accompanied by a TCP Urgent notification. */
public const TCP_DM = 0xf2;
/** @var int No operation. */
public const TCP_NOPT = 0xf1;
/** @var int End of sub-negotiation parameters. */
public const TCP_SE = 0xf0;
public const TCP_BINARY = 0x00;
public const TCP_OPT_ECHO = 0x01;
public const TCP_OPT_SUP_GOAHEAD = 0x03;
public const TCP_OPT_TERMTYPE = 0x18;
public const TCP_OPT_WINDOWSIZE = 0x1f;
public const TCP_OPT_LINEMODE = 0x22;
private bool $option = FALSE;
private string $note;
private string $terminal = '';
public static function send_iac($key): string
{
$send = chr(self::TCP_IAC);
switch ($key) {
case 'are_you_there':
$send .= chr(self::TCP_AYT);
break;
case 'do_echo':
$send .= chr(self::TCP_DO).chr(self::TCP_OPT_ECHO);
break;
case 'dont_echo':
$send .= chr(self::TCP_DONT).chr(self::TCP_OPT_ECHO);
break;
case 'will_echo':
$send .= chr(self::TCP_WILL).chr(self::TCP_OPT_ECHO);
break;
case 'wont_echo':
$send .= chr(self::TCP_WONT).chr(self::TCP_OPT_ECHO);
break;
case 'do_opt_termtype':
$send .= chr(self::TCP_DO).chr(self::TCP_OPT_TERMTYPE);
break;
case 'do_suppress_goahead':
$send .= chr(self::TCP_DO).chr(self::TCP_OPT_SUP_GOAHEAD);
break;
case 'sn_end':
$send .= chr(self::TCP_SE);
break;
case 'sn_start':
$send .= chr(self::TCP_SB);
break;
case 'wont_linemode':
$send .= chr(self::TCP_WONT).chr(self::TCP_OPT_LINEMODE);
break;
case 'will_xmit_binary':
$send .= chr(self::TCP_WILL).chr(self::TCP_BINARY);
break;
default:
throw new \Exception(sprintf('%s:! Unknown key: %s',$key));
}
return $send;
}
public function handle(string $read): string
{
$this->so->log('debug',sprintf('%s:+ Session Char [%02x] (%c)',self::LOGKEY,ord($read),$read),['complete'=>$this->complete,'option'=>$this->option]);
switch (ord($read)) {
// Command being sent.
case self::TCP_IAC:
$this->complete = FALSE;
$this->note = 'IAC ';
break;
case self::TCP_SB:
$this->option = TRUE;
break;
case self::TCP_SE:
$this->option = FALSE;
$this->complete = TRUE;
$this->so->log('debug',sprintf('%s:%% Session Terminal: %s',self::LOGKEY,$this->terminal));
break;
case self::TCP_DO:
$this->note .= 'DO ';
break;
case self::TCP_WILL:
$this->note .= 'WILL ';
break;
case self::TCP_WONT:
$this->note .= 'WONT ';
break;
case self::TCP_OPT_TERMTYPE:
break;
case self::TCP_OPT_ECHO:
$this->note .= 'ECHO';
$this->complete = TRUE;
$this->so->log('debug',sprintf('%s:%% Session Note: [%s]',self::LOGKEY,$this->note));
break;
case self::TCP_OPT_SUP_GOAHEAD:
$this->note .= 'SUPPRESS GO AHEAD';
$this->complete = TRUE;
$this->so->log('debug',sprintf('%s:%% Session Note: [%s]',self::LOGKEY,$this->note));
break;
case self::TCP_OPT_WINDOWSIZE:
$this->note .= 'WINDOWSIZE';
$this->complete = TRUE;
$this->so->log('debug',sprintf('%s:%% Session Note: [%s]',self::LOGKEY,$this->note));
break;
default:
if ($this->option && $read)
$this->terminal .= $read;
else
$this->so->log('debug',sprintf('%s:= Unhandled char in session_init: [%02x] (%c)',self::LOGKEY,ord($read),$read));
}
if ($this->complete)
$this->so->log('debug',sprintf('%s:= TELNET control COMPLETE',self::LOGKEY));
return '';
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Classes\BBS\Control;
use App\Classes\BBS\Control;
/**
* Class Test
*
* This is a test class for Control Validation Processing
*
* @package App\Classes\Control
*/
class Test extends Control
{
public function boot()
{
$this->so->co->send(CLS.HOME.DOWN.CON);
$this->so->co->send('Press 1, or 2, or 4, 0 to end.');
}
// @todo *00/09 doesnt work
public function handle(string $read): string
{
switch ($read)
{
case 0:
$this->complete = TRUE;
$read = '';
break;
case 1:
$this->so->co->send('You pressed ONE.');
$read = '';
break;
case 2:
$this->so->co->send('You pressed TWO.');
$read = '';
break;
case 3:
$this->so->co->send('You pressed THREE.');
$read = '';
break;
case 4:
$this->so->co->send('You pressed FOUR.');
$read = '';
break;
}
return $read;
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Classes\BBS\Exceptions;
use Exception;
class ActionMissingInputsException extends Exception
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Classes\BBS\Exceptions;
use Exception;
class InvalidPasswordException extends Exception
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Classes\BBS\Exceptions;
use Exception;
class NoRouteException extends Exception
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Classes\BBS\Exceptions;
use Exception;
class ParentNotFoundException extends Exception
{
}

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Classes\BBS\Frame;
use Illuminate\Support\Collection;
use App\Classes\BBS\Exceptions\ActionMissingInputsException;
use App\Classes\BBS\Frame\Action\{Login,Register};
use App\Classes\BBS\Server;
use App\Models\User;
abstract class Action
{
private Collection $fields_input;
protected User $uo;
public const actions = [
'login' => Login::class,
'register' => Register::class,
];
protected const fields = [];
abstract public function handle(): bool;
abstract public function preSubmitField(Server $server,Field $field): ?string;
public static function factory(string $class): self
{
if (array_key_exists($class,self::actions)) {
$class = self::actions[$class];
return new $class;
}
throw new \Exception(sprintf('Call to action [%s] doesnt have a class to execute',$class));
}
public function __get(string $key): mixed
{
switch ($key) {
case 'fields_input':
return $this->{$key};
default:
if (($x=$this->fields_input->search(function($item) use ($key) { return $item->name === $key; })) !== FALSE)
return $this->fields_input->get($x)->value;
else
return NULL;
}
}
public function __set(string $key,mixed $value): void
{
switch ($key) {
case 'fields_input':
$this->{$key} = $value;
break;
default:
throw new \Exception('Unknown key: '.$key);
}
}
public function init(): void
{
if (! isset($this->fields_input))
throw new \Exception(sprintf('Missing fields_input in [%s]',get_class($this)));
// First field data element is user, the second is the password
if (count($x=collect(static::fields)->diff($this->fields_input->pluck('name'))))
throw new ActionMissingInputsException(sprintf('Login missing %s',$x->join(',')));
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Classes\BBS\Frame\Action;
use Illuminate\Support\Facades\Hash;
use App\Classes\BBS\Exceptions\{ActionMissingInputsException,InvalidPasswordException};
use App\Classes\BBS\Frame\{Action,Field};
use App\Classes\BBS\Server;
use App\Models\User;
class Login extends Action
{
protected const fields = ['USER','PASS'];
public function __get(string $key): mixed
{
switch ($key) {
case 'user': return $this->uo;
default:
return parent::__get($key);
}
}
/**
* Handle user logins
*
* @return bool
* @throws ActionMissingInputsException
* @throws InvalidPasswordException
*/
public function handle(): bool
{
parent::init();
$this->uo = User::where('name',$this->USER)->orWhere('alias',$this->USER)->firstOrFail();
if (! Hash::check($this->PASS,$this->uo->password))
throw new InvalidPasswordException(sprintf('Password doesnt match for [%s]',$this->USER));
return TRUE;
}
public function preSubmitField(Server $server,Field $field): ?string
{
// Noop
return NULL;
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace App\Classes\BBS\Frame\Action;
use Carbon\Carbon;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Validator;
use App\Classes\BBS\Frame\{Action,Field};
use App\Classes\BBS\Exceptions\ActionMissingInputsException;
use App\Classes\BBS\Server;
use App\Mail\BBS\SendToken;
use App\Models\User;
/**
* Class Register
* This handles the data received for account registration
*
* @package App\Classes\Frame\Action
*/
class Register extends Action
{
protected const fields = ['EMAIL','USER','PASS','FULLNAME','TOKEN'];
private string $token = '';
public function __get(string $key): mixed
{
switch ($key) {
case 'user': return $this->uo;
default:
return parent::__get($key);
}
}
/**
* Handle user logins
*
* @return bool
* @throws ActionMissingInputsException
*/
public function handle(): bool
{
parent::init();
$this->uo = new User;
$this->uo->name = $this->fields_input->where('name','FULLNAME')->first()->value;
$this->uo->email = $this->fields_input->where('name','EMAIL')->first()->value;
$this->uo->email_verified_at = Carbon::now();
$this->uo->password = Hash::make($x=$this->fields_input->where('name','PASS')->first()->value);
$this->uo->active = TRUE;
$this->uo->last_on = Carbon::now();
$this->uo->alias = $this->fields_input->where('name','USER')->first()->value;
$this->uo->save();
return TRUE;
}
public function preSubmitField(Server $server,Field $field): ?string
{
switch ($field->name) {
// Send a token
case 'EMAIL':
// Make sure we got an email address
if (Validator::make(['email'=>$field->value],[
'email'=>'email',
])->fails()) {
return 'INVALID EMAIL ADDRESS';
}
// See if the requested email already exists
if (User::where('email',$field->value)->exists())
return 'USER ALREADY EXISTS';
Log::info(sprintf('Sending token to [%s]',$field->value));
$server->sendBaseline(RED.'SENDING TOKEN...');
$this->token = sprintf('%06.0f',rand(0,999999));
$sent = Mail::to($field->value)->send(new SendToken($this->token));
$server->sendBaseline(RED.'SENT');
break;
case 'USER':
if (str_contains($field->value,' '))
return 'NO SPACES IN USER NAMES';
// See if the requested username already exists
if (User::where('alias',$field->value)->exists())
return 'USER ALREADY EXISTS';
// Clear the baseline from EMAIL entry
$server->sendBaseline('');
break;
case 'TOKEN':
if ($field->value !== $this->token)
return 'INVALID TOKEN';
break;
}
return NULL;
}
}

View File

@@ -0,0 +1,290 @@
<?php
namespace App\Classes\BBS\Frame;
use App\Classes\BBS\Page\{Ansi,Viewdata};
use App\Models\BBS\Mode;
class Char {
/** @var int|null Attributes for the character (ie: color) */
private ?int $attr;
/** @var string|null Character to be shown */
private ?string $ch;
public function __construct(string $ch=NULL,int $attr=NULL)
{
$this->ch = $ch;
$this->attr = $attr;
}
public function __get(string $key): mixed
{
switch ($key) {
case 'attr': return $this->attr;
case 'ch': return $this->ch;
default:
throw new \Exception('Unknown key:'.$key);
}
}
public function __isset($key): bool
{
return isset($this->{$key});
}
public function __set(string $key,mixed $value): void
{
switch ($key) {
case 'ch':
if (strlen($value) !== 1)
throw new \Exception(sprintf('CH can only be 1 char: [%s]',$value));
$this->{$key} = $value;
break;
default:
throw new \Exception('Unknown key:'.$key);
}
}
public function __toString()
{
return sprintf('%04x [%s]|',$this->attr,$this->ch);
}
/**
* Return the color codes required to draw the current character
*
* @param Mode $mo Service we are rendering for
* @param int|null $last last rendered char
* @param bool $debug debug mode
* @return string|NULL
* @throws \Exception
*/
public function attr(Mode $mo,int $last=NULL,bool $debug=FALSE): string|NULL
{
$ansi = collect();
if ($debug)
dump('- last:'.$last.', this:'.$this->attr);
switch ($mo->name) {
case 'ansi':
if ($debug) {
dump(' - this BG_BLACK:'.($this->attr & Ansi::BG_BLACK));
dump(' - last BG_BLACK:'.($last & Ansi::BG_BLACK));
dump(' - this HIGH:'.($this->attr & Ansi::HIGH));
dump(' - last HIGH:'.($last & Ansi::HIGH));
dump(' - this BLINK:'.($this->attr & Ansi::BLINK));
dump(' - last BLINK:'.($last & Ansi::BLINK));
}
// If high was in the last, and we dont have high now, we need 0, but we need to turn back on flash if it was there
// If flash was in the last, and we dont have flash now, we need to 0 but we need to turn on high if it was there
$reset = FALSE;
if ((($this->attr & Ansi::BG_BLACK) && (! ($last & Ansi::BG_BLACK)))
|| ((! ($this->attr & Ansi::BLINK)) && ($last & Ansi::BLINK))
|| ((! ($this->attr & Ansi::HIGH)) && ($last & Ansi::HIGH)))
{
$ansi->push(Ansi::I_CLEAR_CODE);
$reset = TRUE;
$last = Ansi::BG_BLACK|Ansi::LIGHTGRAY;
}
if (($this->attr & Ansi::HIGH)
&& ((($this->attr & Ansi::HIGH) !== ($last & Ansi::HIGH)) || ($reset && ($last & Ansi::HIGH)))) {
$ansi->push(Ansi::I_HIGH_CODE);
}
if (($this->attr & Ansi::BLINK)
&& ((($this->attr & Ansi::BLINK) !== ($last & Ansi::BLINK)) || ($reset && ($last & Ansi::BLINK)))) {
$ansi->push(Ansi::I_BLINK_CODE);
}
$c = ($this->attr & 0x07);
$l = ($last & 0x07);
// Foreground
switch ($c) {
case Ansi::BLACK:
$r = Ansi::FG_BLACK_CODE;
break;
case Ansi::RED:
$r = Ansi::FG_RED_CODE;
break;
case Ansi::GREEN:
$r = Ansi::FG_GREEN_CODE;
break;
case Ansi::BROWN:
$r = Ansi::FG_BROWN_CODE;
break;
case Ansi::BLUE:
$r = Ansi::FG_BLUE_CODE;
break;
case Ansi::MAGENTA:
$r = Ansi::FG_MAGENTA_CODE;
break;
case Ansi::CYAN:
$r = Ansi::FG_CYAN_CODE;
break;
case Ansi::LIGHTGRAY:
$r = Ansi::FG_LIGHTGRAY_CODE;
break;
}
if ($r && ($c !== $l))
$ansi->push($r);
// Background
if ($this->attr & 0x70) {
$c = ($this->attr & 0x70);
$l = ($last & 0x70);
switch ($this->attr & 0x70) {
case Ansi::BG_BLACK:
$r = Ansi::BG_BLACK_CODE;
break;
case Ansi::BG_RED:
$r = Ansi::BG_RED_CODE;
break;
case Ansi::BG_GREEN:
$r = Ansi::BG_GREEN_CODE;
break;
case Ansi::BG_BROWN:
$r = Ansi::BG_BROWN_CODE;
break;
case Ansi::BG_BLUE:
$r = Ansi::BG_BLUE_CODE;
break;
case Ansi::BG_MAGENTA:
$r = Ansi::BG_MAGENTA_CODE;
break;
case Ansi::BG_CYAN:
$r = Ansi::BG_CYAN_CODE;
break;
case Ansi::BG_LIGHTGRAY:
$r = Ansi::BG_LIGHTGRAY_CODE;
break;
}
if ($r && ($c !== $l))
$ansi->push($r);
}
if ($debug)
dump([' - ansi:' =>$ansi]);
return $ansi->count() ? sprintf('%s[%sm',($debug ? '': "\x1b"),$ansi->join(';')) : NULL;
case 'viewdata':
if ($debug)
dump(sprintf('Last: %02x, Attr: %02x',$last,$this->attr));
switch ($this->attr) {
// \x08
case Viewdata::BLINK:
$r = Viewdata::I_BLINK_CODE;
break;
// \x09
case Viewdata::STEADY:
$r = Viewdata::I_STEADY;
break;
// \x0c
case Viewdata::NORMAL:
$r = Viewdata::I_NORMAL;
break;
// \x0d
case Viewdata::DOUBLE:
$r = Viewdata::I_DOUBLE_CODE;
break;
// \x18
case Viewdata::CONCEAL:
$r = Viewdata::I_CONCEAL;
break;
// \x19
case Viewdata::BLOCKS:
$r = Viewdata::I_BLOCKS;
break;
// \x1a
case Viewdata::SEPARATED:
$r = Viewdata::I_SEPARATED;
break;
// \x1c
case Viewdata::BLACKBACK:
$r = Viewdata::I_BLACKBACK;
break;
// \x1d
case Viewdata::NEWBACK:
$r = Viewdata::I_NEWBACK;
break;
// \x1e
case Viewdata::HOLD:
$r = Viewdata::I_HOLD;
break;
// \x1f
case Viewdata::RELEASE:
$r = Viewdata::I_REVEAL;
break;
// Not handled
// \x0a-b,\x0e-f,\x1b
case 0xff00:
dump($this->attr);
break;
default:
$mosiac = ($this->attr & Viewdata::MOSIAC);
$c = ($this->attr & 0x07);
if ($debug)
dump(sprintf('Last: %02x, Attr: %02x, Color: %02x',$last,$this->attr,$c));
// Color control \x00-\x07, \x10-\x17
switch ($c) {
/*
case Viewdata::BLACK:
$r = Viewdata::FG_BLACK_CODE;
break;
*/
case Viewdata::RED:
$r = $mosiac ? Viewdata::MOSIAC_RED_CODE : Viewdata::FG_RED_CODE;
break;
case Viewdata::GREEN:
$r = $mosiac ? Viewdata::MOSIAC_GREEN_CODE : Viewdata::FG_GREEN_CODE;
break;
case Viewdata::YELLOW:
$r = $mosiac ? Viewdata::MOSIAC_YELLOW_CODE : Viewdata::FG_YELLOW_CODE;
break;
case Viewdata::BLUE:
$r = $mosiac ? Viewdata::MOSIAC_BLUE_CODE : Viewdata::FG_BLUE_CODE;
break;
case Viewdata::MAGENTA:
$r = $mosiac ? Viewdata::MOSIAC_MAGENTA_CODE : Viewdata::FG_MAGENTA_CODE;
break;
case Viewdata::CYAN:
$r = $mosiac ? Viewdata::MOSIAC_CYAN_CODE : Viewdata::FG_CYAN_CODE;
break;
case Viewdata::WHITE:
$r = $mosiac ? Viewdata::MOSIAC_WHITE_CODE : Viewdata::FG_WHITE_CODE;
break;
default:
if ($debug)
dump('Not a color?:'.$c);
return NULL;
}
}
if ($debug)
dump(sprintf('= result: ESC[%s](%02x) for [%s]',chr($r),$r,$this->ch));
return chr($r);
default:
throw new \Exception($this->type.': has not been implemented');
}
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace App\Classes\BBS\Frame;
use Illuminate\Support\Arr;
final class Field
{
private array $attributes = [];
private const attributes = [
'attribute', // Color attribute when rendering values
'pad', // Pad character remaining characters up to length
'size', // Size of the field
'name', // Field name
'type', // Type of field
'value', // Current value
'x', // X position in the frame
'y', // Y position in the frame
];
/** @var string[] Attributes that should be masked */
private const mask = [
'p',
];
private const mask_attribute = '*';
public function __construct(array $values)
{
array_walk($values,function($value,$key) {
$this->{$key} = $value;
});
}
public function __get($key): mixed
{
switch ($key) {
case 'can_add':
return strlen($this->value) < $this->size;
case 'mask':
return in_array($this->type,self::mask) ? '*' : NULL;
case 'X':
return $this->x+strlen($this->value);
default:
return Arr::get($this->attributes,$key);
}
}
public function __isset($key): bool
{
return isset($this->attributes[$key]);
}
public function __set($key,$value): void
{
if (! in_array($key,self::attributes))
throw new \Exception('Unknown attribute key:'.$key);
$this->attributes[$key] = $value;
}
/**
* Append a char to the value, only if there is space to do so
*
* @param string $char
* @return bool
*/
public function append(string $char): bool
{
if (is_null($this->value))
$this->clear();
if ($this->can_add) {
$this->value .= $char;
return TRUE;
}
return FALSE;
}
/**
* Clear the field value
*
* @return void
*/
public function clear(): void
{
$this->value = '';
}
/**
* Delete a character from the value, only if there are chars to do so
*
* @return bool
*/
public function delete(): bool
{
if (strlen($this->value)) {
$this->value = substr($this->value,0,-1);
return TRUE;
}
return FALSE;
}
}

628
app/Classes/BBS/Page.php Normal file
View File

@@ -0,0 +1,628 @@
<?php
namespace App\Classes\BBS;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use App\Classes\BBS\Exceptions\{NoRouteException,ParentNotFoundException};
use App\Classes\BBS\Frame\{Action,Field};
use App\Models\BBS\{Frame,Mode};
use App\Models\User;
/**
* The current page object
*
* @property page The full page number requested
*/
abstract class Page
{
/**
* Color attributes can fit in an int
* + Bit 0-2 off = Black foreground
* + Foreground colors bits (0-2)
* + High Intensity 1 bit (3)
* + Bit 4-6 off = Black background
* + Background colors bits (4-6)
* + Flash 1 bit (7)
*/
public const BLINK = 1<<7; /* blink bit */
public const HIGH = 1<<3; /* high intensity (bright) foreground bit */
/* foreground colors */
public const BLACK = 0; /* dark colors (HIGH bit unset) */
public const BLUE = 1;
public const GREEN = 2;
public const CYAN = 3;
public const RED = 4;
public const MAGENTA = 5;
public const BROWN = 6;
public const LIGHTGRAY = 7;
public const DARKGRAY = self::HIGH | self::BLACK; /* light colors (HIGH bit set) */
public const LIGHTBLUE = self::HIGH | self::BLUE;
public const LIGHTGREEN = self::HIGH | self::GREEN;
public const LIGHTCYAN = self::HIGH | self::CYAN;
public const LIGHTRED = self::HIGH | self::RED;
public const LIGHTMAGENTA = self::HIGH | self::MAGENTA;
public const YELLOW = self::HIGH | self::BROWN;
public const WHITE = self::HIGH | self::LIGHTGRAY;
public const BG_BLACK = 0x100; /* special value for ansi() */
public const BG_BLUE = (self::BLUE<<4);
public const BG_GREEN = (self::GREEN<<4);
public const BG_CYAN = (self::CYAN<<4);
public const BG_RED = (self::RED<<4);
public const BG_MAGENTA = (self::MAGENTA<<4);
public const BG_BROWN = (self::BROWN<<4);
public const BG_LIGHTGRAY = (self::LIGHTGRAY<<4);
public const FRAMETYPE_INFO = 'i';
public const FRAMETYPE_ACTION = 'a';
public const FRAMETYPE_RESPONSE = 'r';
public const FRAMETYPE_LOGIN = 'l';
public const FRAMETYPE_TERMINATE = 't';
public const FRAMETYPE_EXTERNAL = 'x';
private int $frame;
private string $index;
/** @var Mode Our BBS mode model object */
protected Mode $mo;
/** @var Frame|null Our frame model object */
private ?Frame $fo = NULL;
/** @var Collection Users page retrieval history */
private Collection $history;
/* Our window layout */
protected Window $layout;
private Window $content;
private Window $header;
private Window $provider;
private Window $pagenum;
private Window $unit;
private bool $showheader = FALSE;
/** @var array Our compiled page */
protected array $build;
/* Fields */
// Current field being edited
private ?int $field_active = NULL;
/** @var Collection Dynamic fields that are pre-populated with system data */
protected Collection $fields_dynamic;
/** @var Collection Input fields take input from the user */
protected Collection $fields_input;
protected bool $debug;
abstract public function attr(array $field): string;
abstract public function parse(string $contents,int $width,int $yoffset=0,int $xoffset=0,?int $debug=NULL): array;
abstract public static function strlenv($text): int;
public function __construct(int $frame,string $index='a',bool $debug=FALSE)
{
$this->debug = $debug;
$this->layout = new Window(1,1,static::FRAME_WIDTH,static::FRAME_HEIGHT+1,'LAYOUT',NULL,$debug);
$this->header = new Window(1,1,static::FRAME_WIDTH,1,'HEADER',$this->layout,$debug);
//dump(['this'=>get_class($this),'header_from'=>$this->header->x,'header_to'=>$this->header->bx,'width'=>$this->header->width]);
// Provider can use all its space
$this->provider = new Window(1,1,static::FRAME_PROVIDER_LENGTH,1,'PROVIDER',$this->header,$debug);
//dump(['this'=>get_class($this),'provider_from'=>$this->provider->x,'provider_to'=>$this->provider->bx,'width'=>$this->provider->width]);
// Page number is prefixed with a color change (if required, otherwise a space)
$this->pagenum = new Window($this->provider->bx+1,1,static::FRAME_PAGE_LENGTH,1,'#',$this->header,$debug);
//dump(['this'=>get_class($this),'pagenum_from'=>$this->pagenum->x,'pagenum_to'=>$this->pagenum->bx,'width'=>$this->pagenum->width]);
// Unit is prefixed with a color change (required, since a different color to page)
$this->unit = new Window($this->pagenum->bx+1,1,static::FRAME_COST_LENGTH,1,'$',$this->header,$debug);
//dump(['this'=>get_class($this),'unit_from'=>$this->unit->x,'unit_to'=>$this->unit->bx,'width'=>$this->unit->width]);
$this->content = new Window(1,2,static::FRAME_WIDTH,static::FRAME_HEIGHT,'CONTENT',$this->layout,$debug);
$this->resetHistory();
$this->clear();
$this->goto($frame,$index);
}
public function __get(string $key): mixed
{
switch ($key) {
case 'access' :
case 'id' :
case 'cls':
case 'cost':
case 'created_at':
case 'public' :
case 'type' :
return $this->fo?->{$key};
case 'cug': return $this->fo?->cug_id;
case 'frame':
case 'index':
return $this->{$key};
case 'next': return ($this->index < 'z') ? chr(ord($this->index)+1) : $this->index;
case 'prev': return ($this->index > 'a') ? chr(ord($this->index)-1) : $this->index;
case 'page': return sprintf('%d%s',$this->frame,$this->index);
case 'height': return $this->layout->height;
case 'width': return $this->layout->width;
case 'fields_input': return $this->fields_input;
case 'field_current': return (! is_null($this->field_active)) ? $this->fields_input->get($this->field_active): NULL;
default:
throw new \Exception('Unknown key: '.$key);
}
}
public function __set(string $key,mixed $value): void
{
switch ($key) {
case 'showheader':
$this->{$key} = $value;
break;
default:
throw new \Exception('Unknown key: '.$key);
}
}
public function __toString(): string
{
return $this->display()->join("");
}
/* METHODS */
/**
* Return a list of alternative versions of this frame.
*
* @todo: Need to adjust to not include access=0 frames unless owner
*/
public function alts(): Collection
{
return Frame::where('frame',$this->frame)
->where('index',$this->index)
->where('id','<>',$this->fo->id)
->where('mode_id',$this->id)
->where('access',1)
->limit(9)
->get();
}
private function atcode(string $name,int $length,mixed $pad=' '): string
{
switch ($name) {
case 'NODE':
$result = '00010001';
break;
case 'DATETIME':
$result = Carbon::now()->toRfc822String();
break;
case 'DATE':
$result = Carbon::now()->format('Y-m-d');
break;
case 'TIME':
$result = Carbon::now()->format('H:ia');
break;
default:
$result = $name;
}
if (strlen($result) < abs($length) && $pad)
$result = ($length < 0)
? Str::padLeft($result,abs($length),$pad)
: Str::padRight($result,abs($length),$pad);
return $result;
}
/**
* History go back to previous page
*
* @return bool
*/
public function back(): bool
{
if ($this->history->count() > 1) {
$this->history->pop();
$this->fo = $this->history->last();
return TRUE;
}
return FALSE;
}
/**
* Parse a page, extracting fields and formatting into our Window objects
*
* @param bool $force
* @return array
* @throws \Exception
*/
public function build(bool $force=FALSE): array
{
if ($this->build && ! $force)
throw new \Exception('Refusing to build without force.');
$this->load();
$test = FALSE;
$this->provider->content = $this->parse(($test ? chr(0x02).'T'.chr(0x03).'B'.chr(0x04) : 'TB').'A'.($test ? ' - 12345678901234567890123456789012345678901234567890123456' : ''),static::FRAME_PROVIDER_LENGTH,$this->provider->y,$this->provider->x);
$this->pagenum->content = $this->parse($this->color_page.($test ? '123456789012345a' : $this->page),static::FRAME_SPACE+static::FRAME_PAGE_LENGTH,$this->pagenum->y,$this->pagenum->x);
$this->unit->content = $this->parse($this->color_unit.Str::padLeft(($this->cost+($test ? 1234 : 0)).'c',static::FRAME_COST_LENGTH-1,' '),static::FRAME_SPACE+static::FRAME_COST_LENGTH,$this->unit->y,$this->unit->x);
$this->content->content = $this->parse($this->fo->content,static::FRAME_WIDTH,$this->content->y,$this->content->x);
$this->header->visible = ($this->showheader || $test);
$this->build_system_fields();
$this->build = $this->layout->build(1,1,$this->debug);
// Add our dynamic values
$fields = $this->fields_dynamic->filter(function($item) { return $item->value; });
Log::channel('bbs')->debug(sprintf('There are [%d] dynamic fields to populate',$fields->count()));
if ($fields->count())
$this->fields_insert($fields);
// Add our input fields
$fields = $this->fields_input->filter(function($item) { return is_null($item->value); });
Log::channel('bbs')->debug(sprintf('There are [%d] input fields to setup',$fields->count()));
if ($fields->count())
$this->fields_insert($fields);
return $this->build;
}
// @todo To complete - some of these came from SBBS and are not valid here
private function build_system_fields(): void
{
// Fields we can process automatically
$auto = ['NODE','DATETIME','DATE','TIME','REALNAME','BBS'];
$df = $this->fields_dynamic->filter(function($item) { return is_null($item->value); });
if (! $df->count())
return;
foreach ($df as $field) {
if (in_array($field->name,$auto))
$this->field_dynamic($field->name,$this->atcode($field->name,$field->size,$field->pad));
}
}
private function clear(): void
{
$this->build = [];
$this->fields_dynamic = collect();
$this->fields_input = collect();
$this->fieldReset();
}
// Insert our *_field data (if it is set)
public function display(): Collection
{
if (! $this->build)
throw new \Exception('Page not ready');
// build
$display = $this->build;
// populate dynamic fields - refresh dynamic fields if 09, otherwise show previous compiled with 00
// check if there are any dynamic fields with no values
switch ($this->mo->name) {
case 'ansi':
$new_line = NULL;
$shownullchars = TRUE;
break;
case 'viewdata':
$new_line = static::BG_BLACK|static::WHITE;
$shownullchars = FALSE;
break;
default:
throw new \Exception(sprintf('Dont know how to display a [%s] page',$this->mo->name));
}
$result = collect();
$last = $new_line;
if ($this->debug)
dump(['page-width'=>$this->width,'page-height'=>$this->height]);
// render
for ($y=1;$y<=$this->height;$y++) {
$line = '';
if ($new_line)
$last = $new_line;
if ($this->debug)
dump('============== ['.$y.'] ===============');
$x = 1;
while ($x <= $this->width) {
if ($this->debug)
dump('* CELL : y:'.$y.', x:'.$x);
// The current char value
$char = (isset($display[$y]) && isset($display[$y][$x])) ? $display[$y][$x] : NULL;
if ($this->debug)
dump(' - CHAR : '.(! is_null($char) ? $char->ch : 'undefined').', ATTR:'.(! is_null($char) ? $char->attr : 'undefined').', LAST:'.$last);
if ($this->debug) {
dump('-------- ['.$x.'] ------');
dump('y:'.$y.',x:'.$x.', attr:'.(! is_null($char) ? $char->attr : 'undefined'));
}
// Only write a new attribute if it has changed (and not Videotex)
if ($last !== $char->attr) {
// The current attribute for this character
$attr = is_null($char) ? NULL : $char->attr($this->mo,$last,$this->debug);
switch ($this->mo->name) {
case 'ansi':
// If the attribute is null, we'll write our default attribute
if (is_null($attr))
$line .= ''; #static::BG_BLACK|static::LIGHTGRAY;
else
$line .= (! is_null($attr)) ? $attr : '';
break;
case 'viewdata':
// If the attribute is null, we'll ignore it since we are drawing a character
if (! is_null($attr)) {
if ($this->debug)
dump(sprintf('= SEND attr:%02x, last: %02x [%s] (%s)',ord($attr),$last,$char->ch,serialize($attr)));
$line .= "\x1b".$attr;
//$x++;
}
break;
default:
throw new \Exception(sprintf('[%s] has not been implemented',$this->mo->name));
}
}
if (! is_null($char->ch)) {
if ($this->debug)
dump(' = SEND CHAR :'.$char->ch.', attr:'.$char->attr.', last:'.$last);
$line .= $char->ch;
} else if ($shownullchars || ((is_null($char->ch) && is_null($char->attr)))) {
if ($this->debug)
dump(' = CHAR UNDEFINED');
$line .= ' ';
}
$last = $char->attr;
$x++;
}
if ($this->debug)
dump(['line'=>$line]);
$result->push($line);
if ($this->debug && ($y > $this->debug))
exit(1);
}
return $result;
}
/**
* Update a dynamic field with a value
*
* @param $name
* @param $value
* @return void
* @throws \Exception
*/
private function field_dynamic($name,$value): void
{
if (($x=$this->fields_dynamic->search(function($item) use ($name) { return $item->name === $name; })) !== FALSE) {
$field = $this->fields_dynamic->get($x);
// Store our value
$field->value = $value;
} else {
throw new \Exception(sprintf('Dynamic field: [%s], doesnt exist?',$name));
}
}
private function fields_insert($fields) {
foreach ($fields as $field) {
if (is_null($field->value))
continue;
$content = str_split($field->value);
$y = $field->y;
$x = $field->x;
for ($x;$x < $field->x+abs($field->size);$x++) {
$index = $x-$field->x;
if (isset($content[$index]))
$this->build[$y][$x]->ch = ($field->type !== 'p') ? $content[$index] : '*';
else
$this->build[$y][$x]->ch = $field->pad;
}
}
}
public function fieldReset(): void
{
$this->field_active = NULL;
foreach ($this->fields_input as $field)
$field->value = NULL;
}
public function fieldNext(): Field|NULL
{
if ($this->fields_input->count()) {
if (is_null($this->field_active))
$this->field_active = 0;
else
$this->field_active++;
return $this->fields_input->get($this->field_active);
} else
return NULL;
}
/**
* Load a frame by it's ID.
*
* @param int $id
* @return void
*/
public function get(int $id): void
{
$this->po->findOrFail($id);
$this->frame = $this->po->frame;
$this->index = $this->po->index;
}
/**
* Go to a specific frame
*
* @param int $frame
* @param string $index
* @return void
* @throws \Exception
*/
public function goto(int $frame,string $index='a'): void
{
if (strlen($index) !== 1)
throw new \Exception('Invalid index:'.$index);
$this->frame = $frame;
$this->index = $index;
$this->fo = NULL;
}
public function haveNext(): bool
{
return $this->fo
? Frame::where('frame',$this->frame)
->where('index',$this->next)
->where('mode_id',$this->fo->mode_id)
->exists()
: FALSE;
}
public function isCug(int $cug): bool
{
return $this->cug === $cug;
}
// @todo To implement
public function isOwner(User $o): bool
{
return FALSE;
}
public function isRoute(int $route): bool
{
return is_numeric($this->fo->{sprintf('r%d',$route)});
}
/**
* Load a frame, throw a model not found exception if it doesnt exist
*
* @return void
*/
public function load(): void
{
$this->fo = Frame::where('mode_id',$this->mo->id)
->where('frame',$this->frame)
->where('index',$this->index)
->orderBy('created_at','DESC')
->firstOrFail();
$this->history->push($this->fo);
$this->clear();
}
public function method(int $route): ?Action
{
if (($x=($this->fo->{sprintf('r%d',$route)})) && (! $this->isRoute($route)))
return Action::factory($x);
return NULL;
}
public function new(int $frame,string $index='a'): void
{
$this->frame = $frame;
$this->index = $index;
$this->fo = new Frame;
// Make sure parent frame exists
if (($this->index !== 'a') && (! Frame::where('frame',$this->frame)->where('index',$this->prev)->where('mode',$this->mo->id)->exists()))
throw new ParentNotFoundException(sprintf('Parent %d%s doesnt exist',$frame,$index));
}
public function next(): void
{
$this->index = $this->next;
$this->fo = NULL;
}
/**
* Clear a user's history
*
* @return void
*/
public function resetHistory(): void
{
$this->history = collect();
}
public function route(int $route): void
{
if ($this->isRoute($route)) {
$this->frame = (int)$this->fo->{sprintf('r%d',$route)};
$this->index = 'a';
$this->fo = NULL;
} else {
throw new NoRouteException('Invalid route '.$route);
}
}
public function prev(): void
{
$this->index = $this->prev;
$this->fo = NULL;
}
}

View File

@@ -0,0 +1,433 @@
<?php
namespace App\Classes\BBS\Page;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use App\Classes\BBS\Frame\{Char,Field};
use App\Classes\BBS\Page;
use App\Models\BBS\Mode;
class Ansi extends Page
{
protected const FRAME_WIDTH = 80;
protected const FRAME_HEIGHT = 22;
protected const FRAME_PROVIDER_LENGTH = 55;
protected const FRAME_PAGE_LENGTH = 17; // Full space for page number + space at beginning (as would be displayed by viewdata)
protected const FRAME_COST_LENGTH = 8; // Full space for cost + space at beginning (as would be displayed by viewdata)
protected const FRAME_SPACE = 1; // Since colors dont take a space, this is to buffer a space
public const ESC = 27;
public const I_CLEAR_CODE = 0;
public const I_HIGH_CODE = 1;
public const I_BLINK_CODE = 5;
public const FG_WHITE_CODE = self::FG_LIGHTGRAY_CODE;
public const FG_YELLOW_CODE = self::FG_BROWN_CODE;
public const FG_BLACK_CODE = 30;
public const FG_RED_CODE = 31;
public const FG_GREEN_CODE = 32;
public const FG_BROWN_CODE = 33;
public const FG_BLUE_CODE = 34;
public const FG_MAGENTA_CODE = 35;
public const FG_CYAN_CODE = 36;
public const FG_LIGHTGRAY_CODE = 37;
public const BG_BLACK_CODE = 40;
public const BG_RED_CODE = 41;
public const BG_GREEN_CODE = 42;
public const BG_BROWN_CODE = 43;
public const BG_YELLOW_CODE = self::BG_BROWN_CODE;
public const BG_BLUE_CODE = 44;
public const BG_MAGENTA_CODE = 45;
public const BG_CYAN_CODE = 46;
public const BG_LIGHTGRAY_CODE = 47;
public static function strlenv($text): int
{
return strlen($text ? preg_replace('/'.ESC.'\[[0-9;?]+[a-zA-Z]/','',$text) : $text);
}
public function __construct(int $frame,string $index='a')
{
parent::__construct($frame,$index);
$this->mo = Mode::where('name','Ansi')->single();
}
public function __get(string $key): mixed
{
switch ($key) {
case 'color_page':
return sprintf(" %s[%d;%dm",chr(self::ESC),self::I_HIGH_CODE,self::FG_WHITE_CODE);
case 'color_unit':
return sprintf(" %s[%d;%dm",chr(self::ESC),self::I_HIGH_CODE,self::FG_GREEN_CODE);
default:
return parent::__get($key);
}
}
public function attr(array $field): string
{
return sprintf('%s[%d;%d;%dm',ESC,$field['i'],$field['f'],$field['b']);
}
/**
* This function converts ANSI text into an array of attributes
*
* We include the attribute for every character, so that if a window is placed on top of this window, the edges
* render correctly.
*
* @param string $contents Our ANSI content to convert
* @param int $width Canvas width before we wrap to the next line
* @param int $yoffset fields offset when rendered (based on main window)
* @param int $xoffset fields offset when rendered (based on main window)
* @param int|null $debug Enable debug mode
* @return array
* @throws \Exception
*/
public function parse(string $contents,int $width,int $yoffset=0,int $xoffset=0,?int $debug=NULL): array
{
$result = [];
$lines = collect(explode("\r\n",$contents));
if ($debug)
dump(['lines'=>$lines]);
$i = 0; // Intensity
$bg = self::BG_BLACK; // Background color
$fg = self::LIGHTGRAY; // Foreground color
$attr = $fg + $bg + $i; // Attribute int
$default = ['i'=>0,'f'=>self::FG_LIGHTGRAY_CODE,'b'=>self::BG_BLACK_CODE];
$y = 0; // Line
$saved_x = NULL; // Cursor saved
$saved_y = NULL; // Cursor saved
$ansi = $default; // Our current attribute used for input fields
while ($lines->count() > 0) {
$x = 0;
$line = $lines->shift();
$result[$y+1] = [];
if ($this->debug) dump(['next line'=>$line,'length'=>strlen($line)]);
if (is_numeric($debug) && ($y > $debug)) {
dump(['exiting'=>serialize($debug)]);
exit(1);
}
while (strlen($line) > 0) {
if ($debug)
dump(['y:'=>$y,'attr'=>$attr,'line'=>$line,'length'=>strlen($line)]);
if ($x >= $width) {
$x = 0;
$y++;
}
/* parse an attribute sequence*/
$m = [];
preg_match('/^\x1b\[((\d+)+(;(\d+)+)*)m/U',$line,$m);
if (count($m)) {
$line = substr($line,strlen(array_shift($m)));
// Are values separated by ;
$m = array_map(function($item) { return (int)$item; },explode(';',$m[0]));
// Sort our numbers
sort($m);
// Reset
if ($m[0] === self::I_CLEAR_CODE) {
$bg = self::BG_BLACK;
$fg = self::LIGHTGRAY;
$i = 0;
$ansi = $default;
array_shift($m);
}
// High Intensity
if (count($m) && ($m[0] === self::I_HIGH_CODE)) {
$i += ((($i === 0) || ($i === self::BLINK)) ? self::HIGH : 0);
$ansi['i'] = self::I_HIGH_CODE;
array_shift($m);
}
// Blink
if (count($m) && ($m[0] === self::I_BLINK_CODE)) {
$i += ((($i === 0) || ($i === self::HIGH)) ? self::BLINK : 0);
array_shift($m);
}
// Foreground
if (count($m) && ($m[0] >= self::FG_BLACK_CODE) && ($m[0] <= self::FG_LIGHTGRAY_CODE)) {
$ansi['f'] = $m[0];
switch (array_shift($m)) {
case self::FG_BLACK_CODE:
$fg = self::BLACK;
break;
case self::FG_RED_CODE:
$fg = self::RED;
break;
case self::FG_GREEN_CODE:
$fg = self::GREEN;
break;
case self::FG_YELLOW_CODE:
$fg = self::BROWN;
break;
case self::FG_BLUE_CODE:
$fg = self::BLUE;
break;
case self::FG_MAGENTA_CODE:
$fg = self::MAGENTA;
break;
case self::FG_CYAN_CODE:
$fg = self::CYAN;
break;
case self::FG_LIGHTGRAY_CODE:
$fg = self::LIGHTGRAY;
break;
}
}
// Background
if (count($m) && ($m[0] >= self::BG_BLACK_CODE) && ($m[0] <= self::BG_LIGHTGRAY_CODE)) {
$ansi['b'] = $m[0];
switch (array_shift($m)) {
case self::BG_BLACK_CODE:
$bg = self::BG_BLACK;
break;
case self::BG_RED_CODE:
$bg = self::BG_RED;
break;
case self::BG_GREEN_CODE:
$bg = self::BG_GREEN;
break;
case self::BG_BROWN_CODE:
$bg = self::BG_BROWN;
break;
case self::BG_BLUE_CODE:
$bg = self::BG_BLUE;
break;
case self::BG_MAGENTA_CODE:
$bg = self::BG_MAGENTA;
break;
case self::BG_CYAN_CODE:
$bg = self::BG_CYAN;
break;
case self::BG_LIGHTGRAY_CODE:
$bg = self::BG_LIGHTGRAY;
break;
}
}
$attr = $bg + $fg + $i;
continue;
}
/* parse absolute character position */
$m = [];
preg_match('/^\x1b\[(\d*);?(\d*)[Hf]/',$line,$m);
if (count($m)) {
dump(['Hf'=>$m]); // @todo Remove once validated
$line = substr($line,strlen(array_shift($m)));
$y = (int)array_shift($m);
if (count($m))
$x = (int)array_shift($m)-1;
continue;
}
/* ignore an invalid sequence */
$m = [];
preg_match('/^\x1b\[\?7h/',$line,$m);
if (count($m)) {
$line = substr($line,strlen(array_shift($m)));
continue;
}
/* parse positional sequences */
$m = [];
preg_match('/^\x1b\[(\d+)([A-D])/',$line,$m);
if (count($m)) {
$line = substr($line,strlen(array_shift($m)));
switch ($m[1]) {
/* parse an up positional sequence */
case 'A':
$y -= ($m[0] < 1) ? 0 : $m[0];
break;
/* parse a down positional sequence */
case 'B':
$y += ($m[0] < 1) ? 0 : $m[0];
break;
/* parse a forward positional sequence */
case 'C':
$x += ($m[0] < 1) ? 0 : $m[0];
break;
/* parse a backward positional sequence */
case 'D':
$x -= ($m[0] < 1) ? 0 : $m[0];
break;
}
continue;
}
/* parse a clear screen sequence - we ignore them */
$m = [];
preg_match('/^\x1b\[2J/',$line,$m);
if (count($m)) {
$line = substr($line,strlen(array_shift($m)));
continue;
}
/* parse cursor sequences */
$m = [];
preg_match('/^\x1b\[([su])/',$line,$m);
if (count($m)) {
$line = substr($line,strlen(array_shift($m)));
switch ($m[0]) {
/* parse save cursor sequence */
case 's':
$saved_x = $x;
$saved_y = $y;
break;
/* parse restore cursor sequence */
case 'u':
$x = $saved_x;
$y = $saved_y;
break;
}
continue;
}
/* parse an input field */
// Input field 'FIELD;valueTYPE;input char'
// @todo remove the trailing ESC \ to end the field, just use a control code ^B \x02 (Start of Text) and ^C \x03
$m = [];
preg_match('/^\x1b_([A-Z]+;[0-9a-z]+)([;]?.+)?\x1b\\\/',$line,$m);
if (count($m)) {
$line = substr($line,strlen(array_shift($m)));
// We are interested in our field match
$f = explode(';',array_shift($m));
// First value is the field name
$field = array_shift($f);
// Second value is the length/type of the field, nnX nn=size in chars, X=type (lower case)
$c = [];
preg_match('/([0-9]+)([a-z])/',$xx=array_shift($f),$c);
if (! count($c)) {
Log::channel('bbs')->alert(sprintf('! IF FAILED PARSING FIELD LENGTH/TYPE [%02dx%02d] (%s)',$y,$x,$xx));
break;
}
// Third field is the char to use
$fieldpad = count($f) ? array_shift($f) : '.';
Log::channel('bbs')->info(sprintf('- IF [%02dx%02d], Field: [%s], Length: [%d], Char: [%s]',$y,$x,$c[2],$c[1],$fieldpad));
// Any remaining fields are junk
if (count($f))
Log::channel('bbs')->alert(sprintf('! IGNORING ADDITIONAL IF FIELDS [%02dx%02d] (%s)',$y,$x,join('',$f)));
// If we are padding our field with a char, we need to add that back to $line
// @todo validate if this goes beyond our width (and if scrolling not enabled)
if ($c[1])
$line = str_repeat($fieldpad,$c[1]).$line;
$this->fields_input->push(new Field([
'attribute' => $ansi,
'name' => $field,
'pad' => $fieldpad,
'size' => $c[1],
'type' => $c[2],
'value' => NULL,
'x' => $x+$xoffset,
'y' => $y+$yoffset,
]));
}
/* parse dynamic value field */
// @todo remove the trailing ESC \ to end the field, just use a control code ie: ^E \x05 (Enquiry) or ^Z \x26 (Substitute)
$m = [];
preg_match('/^\x1bX([a-zA-Z._:^;]+[0-9]?;-?[0-9^;]+)([;]?[^;]+)?\x1b\\\/',$line,$m);
if (count($m)) {
$line = substr($line,strlen(array_shift($m)));
// We are interested in our field match
$f = explode(';',array_shift($m));
$pad = Arr::get($f,2,' ');
Log::channel('bbs')->info(sprintf('- DF [%02dx%02d], Field: [%s], Length: [%d], Char: [%s]',$y,$x,$f[0],$f[1],$pad));
// If we are padding our field with a char, we need to add that back to line
// @todo validate if this goes beyond our width (and if scrolling not enabled)
$line = str_repeat($pad,abs($f[1])).$line;
$this->fields_dynamic->push(new Field([
'name' => $f[0],
'pad' => $pad,
'type' => NULL,
'size' => $f[1],
'value' => NULL,
'x' => $x+$xoffset,
'y' => $y+$yoffset,
]));
}
/* set character and attribute */
$ch = $line[0];
$line = substr($line,1);
/* validate position */
if ($y < 0)
$y = 0;
if ($x < 0)
$x = 0;
if ($attr === null)
throw new \Exception('Attribute is null?');
$result[$y+1][$x+1] = new Char($ch,$attr);
$x++;
}
// If we got a self::BG_BLACK|self::LIGHTGRAY ESC [0m, but not character, we include it as it resets any background that was going on
if (($attr === self::BG_BLACK|self::LIGHTGRAY) && isset($result[$y+1][$x]) && ($result[$y+1][$x]->attr !== $attr))
$result[$y+1][$x+1] = new Char(NULL,$attr);
$y++;
}
return $result;
}
}

View File

@@ -0,0 +1,370 @@
<?php
namespace App\Classes\BBS\Page;
use Illuminate\Support\Arr;
use App\Classes\BBS\Frame\{Char,Field};
use App\Classes\BBS\Page;
use App\Models\BBS\Mode;
class Viewdata extends Page
{
protected const FRAME_WIDTH = 40;
protected const FRAME_HEIGHT = 22;
protected const FRAME_PROVIDER_LENGTH = 23;
protected const FRAME_PAGE_LENGTH = 11; // Spec is 9+1 - including our color code.
protected const FRAME_COST_LENGTH = 6; // including our color code
protected const FRAME_SPACE = 0; // Since colors take a space, this is not needed
public const MOSIAC = 0x10;
// Toggles
public const CONCEAL = 0x20;
public const REVEAL = 0x2000; // @temp Turns off Conceal
public const SEPARATED = 0x40;
public const BLOCKS = 0x4000; // @temp Turns off Separated
public const STEADY = 0x8000; // @temp (turn off flash)
public const DOUBLE = 0x100;
public const NORMAL = 0x1000; // @temp Turns off Double Height
public const HOLD = 0x200;
public const RELEASE = 0x20000; // @temp turns off Hold
public const NEWBACK = 0x400;
public const BLACKBACK = 0x800;
//public const ESC = 27;
//public const I_CLEAR_CODE = 0;
//public const I_HIGH_CODE = 1;
public const FG_BLACK_CODE = 0x40;
public const FG_RED_CODE = 0x41;
public const FG_GREEN_CODE = 0x42;
public const FG_YELLOW_CODE = 0x43;
public const FG_BLUE_CODE = 0x44;
public const FG_MAGENTA_CODE = 0x45;
public const FG_CYAN_CODE = 0x46;
public const FG_WHITE_CODE = 0x47;
public const I_BLINK_CODE = 0x48;
public const I_STEADY = 0x49;
public const I_NORMAL = 0x4c;
public const I_DOUBLE_CODE = 0x4d;
public const I_CONCEAL = 0x58;
public const I_BLOCKS = 0x59;
public const I_SEPARATED = 0x5a;
public const I_BLACKBACK = 0x5c;
public const I_NEWBACK = 0x5d;
public const I_HOLD = 0x5e;
public const I_REVEAL = 0x5f;
public const RED = 1;
//public const GREEN = 2;
public const YELLOW = 3;
public const BLUE = 4;
//public const MAGENTA = 5;
public const CYAN = 6;
public const WHITE = 7;
public const MOSIAC_RED_CODE = 0x51;
public const MOSIAC_GREEN_CODE = 0x52;
public const MOSIAC_YELLOW_CODE = 0x53;
public const MOSIAC_BLUE_CODE = 0x54;
public const MOSIAC_MAGENTA_CODE = 0x55;
public const MOSIAC_CYAN_CODE = 0x56;
public const MOSIAC_WHITE_CODE = 0x57; // W
public const input_map = [
'd' => 'DATE',
'e' => 'EMAIL',
'f' => 'FULLNAME',
'n' => 'USER',
'p' => 'PASS',
't' => 'TIME',
'y' => 'NODE',
'z' => 'TOKEN',
];
public static function strlenv($text):int
{
return strlen($text)-substr_count($text,ESC);
}
public function __construct(int $frame,string $index='a')
{
parent::__construct($frame,$index);
$this->mo = Mode::where('name','Viewdata')->single();
}
public function __get(string $key): mixed
{
switch ($key) {
case 'color_page':
return chr(self::WHITE);
case 'color_unit':
return chr(self::GREEN);
default:
return parent::__get($key);
}
}
public function attr(array $field): string
{
// Noop
return '';
}
/**
* This function converts Viewtex BIN data into an array of attributes
*
* With viewdata, a character is used/display regardless of whether it is a control character, or an actual display
* character.
*
* @param string $contents Our ANSI content to convert
* @param int $width Canvas width before we wrap to the next line
* @param int $yoffset fields offset when rendered (based on main window)
* @param int $xoffset fields offset when rendered (based on main window)
* @param int|null $debug Enable debug mode
* @return array
* @throws \Exception
*/
public function parse(string $contents,int $width,int $yoffset=0,int $xoffset=0,?int $debug=NULL): array
{
$result = [];
$lines = collect(explode("\r\n",$contents));
if ($debug)
dump(['lines'=>$lines]);
$i = 0; // Intensity
$bg = self::BG_BLACK; // Background color
$fg = self::WHITE; // Foreground color
$new_line = $fg + $bg + $i; // Attribute int
// Attribute state on a new line
$attr = $new_line;
$y = 0;
while ($lines->count() > 0) {
$x = 0;
$line = $lines->shift();
$result[$y+1] = [];
if ($this->debug)
dump(['next line'=>$line,'length'=>strlen($line)]);
while (strlen($line) > 0) {
if ($debug)
dump(['y:'=>$y,'attr'=>$attr,'line'=>$line,'length'=>strlen($line)]);
if ($x >= $width) {
$x = 0;
// Each new line, we reset the attrs
$attr = $new_line;
$y++;
}
/* parse control codes */
$m = [];
preg_match('/^([\x00-\x09\x0c-\x1a\x1c-\x1f])/',$line,$m);
if (count($m)) {
$line = substr($line,strlen(array_shift($m)));
$attr = 0;
switch ($xx=ord(array_shift($m))) {
case 0x00:
$attr += self::BLACK;
break;
case 0x01:
$attr += self::RED;
break;
case 0x02:
$attr += self::GREEN;
break;
case 0x03:
$attr += self::YELLOW;
break;
case 0x04:
$attr += self::BLUE;
break;
case 0x05:
$attr += self::MAGENTA;
break;
case 0x06:
$attr += self::CYAN;
break;
case 0x07:
$attr += self::WHITE;
break;
case 0x08:
$attr = self::BLINK;
break;
case 0x09:
$attr = self::STEADY;
break;
/*
case 0x0a:
//$attr = self::ENDBOX; // End Box (Unused?)
break;
case 0x0b:
//$attr = self::STARTBOX; // Start Box (Unused?)
break;
*/
case 0x0c:
$attr = self::NORMAL;
break;
case 0x0d:
$attr = self::DOUBLE;
break;
case 0x0e:
$attr = self::NORMAL; // @todo Double Width (Unused)?
break;
case 0x0f:
$attr = self::NORMAL; // @todo Double Width (Unused?)
break;
case 0x10:
$attr = self::MOSIAC|self::BLACK;
break;
case 0x11:
$attr = self::MOSIAC|self::RED;
break;
case 0x12:
$attr = self::MOSIAC|self::GREEN;
break;
case 0x13:
$attr = self::MOSIAC|self::YELLOW;
break;
case 0x14:
$attr = self::MOSIAC|self::BLUE;
break;
case 0x15:
$attr = self::MOSIAC|self::MAGENTA;
break;
case 0x16:
$attr = self::MOSIAC|self::CYAN;
break;
case 0x17:
$attr = self::MOSIAC|self::WHITE;
break;
case 0x18:
$attr = self::CONCEAL;
break;
case 0x19:
$attr = self::BLOCKS;
break;
case 0x1a:
$attr = self::SEPARATED;
break;
/*
// We are using this for field input
case 0x1b:
//$attr = self::NORMAL; // CSI
break;
*/
case 0x1c:
$attr = self::BLACKBACK; // Black Background
break;
case 0x1d:
$attr = self::NEWBACK; // New Background
break;
case 0x1e:
$attr = self::HOLD; // Mosiac Hold
break;
case 0x1f:
$attr = self::RELEASE; // Mosiac Release
break;
// Catch all for other codes
default:
dump(['char'=>$xx]);
$attr = 0xff00;
}
if ($debug)
dump(sprintf('- got control code [%02x] at [%02dx%02d]',$attr,$y,$x));
$result[$y+1][$x+1] = new Char(NULL,$attr);
$x++;
continue;
}
/**
* For response frames, a dialogue field is signalled by a CLS (0x0c) followed by a number of dialogue
* characters [a-z]. The field ends by the first different character from the initial dialogue character.
* The CLS is a "privileged space" and the dialogue characters defined the dialogue field.
*
* Standard dialogue characters:
* + n = name
* + t = telephone number
* + d = date and time
* + a = address
* + anything else free form, typically 'f' is used
*
* Source: Prestel Bulk Update Technical Specification
*/
/* parse an input field */
// Since 0x0c is double, we'll use good ol' ESC 0x1b
$m = [];
preg_match('/^([\x1b|\x9b])([a-z])\2+/',$line,$m);
if (count($m)) {
$line = substr($line,strlen($m[0]));
$len = strlen(substr($m[0],1));
$field = new Field([
'attribute' => [],
'name' => Arr::get(self::input_map,$m[2],$m[2]),
'pad' => '.',
'size' => $len,
'type' => $m[2],
'value' => NULL,
'x' => $x+$xoffset,
'y' => $y+$yoffset,
]);
(($m[1] === "\x1b") ? $this->fields_input : $this->fields_dynamic)->push($field);
$result[$y+1][++$x] = new Char(' ',$attr); // The \x1b|\x9b is the privileged space.
for ($xx=0;$xx<$len;$xx++)
$result[$y+1][$x+1+$xx] = new Char('.',$attr);
$x += $len;
continue;
}
/* set character and attribute */
$ch = $line[0];
$line = substr($line,1);
if ($debug)
dump(sprintf('Storing [%02xx%02x] [%s] with [%02x]',$y,$x,$ch,$attr));
/* validate position */
if ($y < 0)
$y = 0;
if ($x < 0)
$x = 0;
if ($attr === null)
throw new \Exception('Attribute is null?');
$result[$y+1][$x+1] = new Char($ch,$attr);
$x++;
}
// Each new line, we reset the attrs
$attr = $new_line;
$y++;
}
return $result;
}
}

1231
app/Classes/BBS/Server.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,87 @@
<?php
namespace App\Classes\BBS\Server;
use App\Classes\BBS\Server as AbstractServer;
use App\Classes\Sock\SocketClient;
class Ansitex extends AbstractServer
{
protected const LOGKEY = 'BAS';
/* CONSTS */
public const PORT = 23;
protected function init(SocketClient $client)
{
define('ESC', chr(27));
define('CON', ESC.'[?25h'); // Cursor On
define('COFF', ESC.'[?25l'); // Cursor Off
define('CSAVE', ESC.'[s'); // Save Cursor position
define('CRESTORE',ESC.'[u'); // Restore to saved position
define('HOME', ESC.'[0;0f');
define('LEFT', ESC.'[D'); // Move Cursor
define('RIGHT', ESC.'[C'); // Move Cursor
define('DOWN', ESC.'[B'); // Move Cursor
define('UP', ESC.'[A'); // Move Cursor
define('CR', chr(13));
define('LF', chr(10));
define('BS', chr(8));
define('CLS', ESC.'[2J');
define('HASH', '#'); // Enter
define('STAR', '*'); // Star Entry
define('SPACE', ' '); // Space (for compatibility with Videotex)
// NOTE: This consts are effective output
define('RESET', ESC.'[0;39;49m');
define('RED', ESC.'[0;31m');
define('GREEN', ESC.'[0;32m');
define('YELLOW', ESC.'[1;33m');
define('BLUE', ESC.'[0;34m');
define('MAGENTA', ESC.'[0;35m');
define('CYAN', ESC.'[0;36m');
define('WHITE', ESC.'[1;37m');
define('NEWBG', '');
// Compatibility attributes (to Videotex).
define('R_RED', RED.SPACE);
define('R_GREEN', GREEN.SPACE);
define('R_YELLOW', YELLOW.SPACE);
define('R_BLUE', BLUE.SPACE);
define('R_MAGENTA', MAGENTA.SPACE);
define('R_CYAN', CYAN.SPACE);
define('R_WHITE', WHITE.SPACE);
//define('FLASH',chr(8));
// Keyboard presses
// @todo Check where these are used vs the keys defined above?
define('KEY_DELETE', chr(8));
define('KEY_LEFT', chr(136));
define('KEY_RIGHT', chr(137));
define('KEY_DOWN', chr(138));
define('KEY_UP', chr(139));
parent::init($client);
}
function moveCursor($x,$y): string
{
return ESC.'['.$y.';'.$x.'f';
}
// Abstract function
public function sendBaseline(string $text,bool $reposition=FALSE)
{
$this->client->send(CSAVE.ESC.'[24;0f'.RESET.SPACE.$text.
($this->blp > $this->po->strlenv(SPACE.$text)
? str_repeat(' ',$this->blp-$this->po->strlenv(SPACE.$text)).
($reposition ? ESC.'[24;0f'.str_repeat(RIGHT,$this->po->strlenv(SPACE.$text)) : CRESTORE)
: ($reposition ? '' : CRESTORE)),
static::TIMEOUT
);
$this->blp = $this->po->strlenv(SPACE.$text);
$this->baseline = $text;
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace App\Classes\BBS\Server;
use App\Classes\BBS\Server as AbstractServer;
use App\Classes\Sock\SocketClient;
class Videotex extends AbstractServer
{
protected const LOGKEY = 'BVS';
/* CONSTS */
public const PORT = 516;
protected function init(SocketClient $client)
{
define('ESC', chr(27));
define('CON', chr(17)); // Cursor On
define('COFF', chr(20)); // Cursor Off
define('HOME', chr(30));
define('LEFT', chr(8)); // Move Cursor
define('RIGHT', chr(9)); // Move Cursor
define('DOWN', chr(10)); // Move Cursor
define('UP', chr(11)); // Move Cursor
define('CR', chr(13));
define('LF', chr(10));
define('CLS', chr(12));
define('HASH', '_'); // Enter
define('STAR', '*'); // Star Entry
define('SPACE', ''); // Space
// NOTE: This consts are effective output
define('RESET', '');
define('RED', ESC.'A');
define('GREEN', ESC.'B');
define('YELLOW', ESC.'C');
define('BLUE', ESC.'D');
define('MAGENTA', ESC.'E');
define('CYAN', ESC.'F');
define('WHITE', ESC.'G');
define('NEWBG', ESC.']');
// Raw attributes - used when storing frames.
define('R_RED', chr(1));
define('R_GREEN', chr(2));
define('R_YELLOW', chr(3));
define('R_BLUE', chr(4));
define('R_MAGENTA', chr(5));
define('R_CYAN', chr(6));
define('R_WHITE', chr(7));
define('FLASH', chr(8));
define('KEY_DELETE', chr(0x7f));
define('KEY_LEFT', chr(0x08));
define('KEY_RIGHT', chr(0x09));
define('KEY_DOWN', chr(0x0a));
define('KEY_UP', chr(0x0b));
parent::init($client);
}
public function moveCursor($x,$y): string
{
// Take the shortest path.
if ($y < 12) {
return HOME.
(($x < 21)
? str_repeat(DOWN,$y-1).str_repeat(RIGHT,$x)
: str_repeat(DOWN,$y).str_repeat(LEFT,40-$x));
} else {
return HOME.str_repeat(UP,24-$y+1).
(($x < 21)
? str_repeat(RIGHT,$x)
: str_repeat(LEFT,40-$x));
}
}
public function sendBaseline(string $text,bool $reposition=FALSE) {
$this->client->send(HOME.UP.$text.
($this->blp > $this->po->strlenv($text)
? str_repeat(' ',$this->blp-$this->po->strlenv($text)).
($reposition ? HOME.UP.str_repeat(RIGHT,$this->po->strlenv($text)) : '')
: ''),
static::TIMEOUT
);
$this->blp = $this->po->strlenv($text);
}
}

365
app/Classes/BBS/Window.php Normal file
View File

@@ -0,0 +1,365 @@
<?php
namespace App\Classes\BBS;
use Illuminate\Support\Collection;
use App\Classes\BBS\Frame\Char;
/**
* Windows are elements of a Page object
*
* @param int $x - (int) starting x of it's parent [1..]
* @param int $y - (int) starting y of it's parent [1..]
* @param int $width - (int) full width of the window (text content will be smaller if there are scroll bars/boarder)
* @param int $height - (int) full height of the window (text content will be smaller if there are scroll bars/boarder)
* @param string $name - (string) internal name for the window (useful for debugging)
* @param Window $parent - (object) parent of this window
* @param bool $debug - (int) debug mode, which fills the window with debug content
*
* Pages have the following attributes:
* - bx/by - (int) right/bottom most boundary of the window representing the start + width/height of the window
* - child - (array) children in this window
* - height - (int) Window's height
* - name - (string) Windows name (useful for internal debugging)
* - parent - (object) Parent that this window belongs to
* - x/y - (int) start position of the window
* - visible - (bool) whether this window is visible
* - width - (int) Window's width
* - z - (int) Window's depth indicator
*
* Windows have the following public functions
* - build - Compile the frame for rendering
* - debug - Useful for debugging with properties of this Window
* - draw - Draw a part of this Window
*/
class Window
{
/** @var int X offset of parent that the canvas starts [1..width] */
private int $x;
/** @var int Y offset of parent that the canvas starts [1..height] */
private int $y;
/** @var int Window top-bottom position, higher z is shown [0..] */
private int $z = 0;
/** @var int When canvas width > width, this is the offset we display [0..] */
private int $ox = 0;
/** @var int When canvas height > height, this is the offset we display [0..] */
private int $oy = 0;
/** @var int Display Width + (1 char if scrollbars = true) */
private int $width;
/** @var int Display Height */
private int $height;
/** @var int Width of Canvas (default display width) */
private int $canvaswidth;
/** @var int Height of Canvas (default display height) */
private int $canvasheight;
/** @var array Window content - starting at 0,0 = 1,1 */
public array $content = [];
/** @var bool Window visible */
private bool $visible = TRUE;
/** @var string Window name */
private string $name;
/** @var bool Can this frame move outside the parent */
private bool $checkbounds = TRUE;
/** @var bool Can the content scroll vertically (takes up 1 line) [AUTO DETERMINE IF canvas > width] */
private bool $v_scroll = TRUE;
/** @var bool Can the content scroll horizontally (takes up 1 char) [AUTO DETERMINE IF canvas > height] */
private bool $h_scroll = FALSE;
/** @var int|bool Overflowed content is rendered with the next page */
private bool $pageable = FALSE;
private Page|Window|NULL $parent;
private Collection $child;
private bool $debug;
/*
Validation to implement:
+ X BOUNDARY
- x cannot be < parent.x if checkbounds is true [when moving window]
- x+width(-1 if h_scroll is true) cannot be greater than parent.width if checkbounds is true
- v_scroll must be true for canvaswidth > width
- when scrolling ox cannot be > width-x
- when layout.pageable is true, next page will only have windows included that have a y in the range
ie: if height is 44 (window is 22), next page is 23-44 and will only include children where y=23-44
+ Y BOUNDARY
- y cannot be < parent.y if checkbounds is true [when moving window]
- y+height(-1 if v_scroll is true) cannot be greater than parent.height if checkbounds is true
- h_scroll must be true for canvasheight > height
- when scrolling oy cannot be > height-y
- when layout.pageable is true, children height cannot be greater than parent.height - y.
*/
public function __construct(int $x,int $y,int $width,int $height,string $name,Window|Page $parent=NULL,bool $debug=FALSE) {
$this->x = $x;
$this->y = $y;
$this->name = $name;
$this->parent = $parent;
$this->debug = $debug;
$this->child = collect();
if ($parent instanceof self) {
$this->z = $parent->child->count()+1;
$this->parent = $parent;
$this->parent->child->push($this);
// Check that our height/widths is not outside our parent
if (($this->x < 1) || ($width > $this->parent->width))
throw new \Exception(sprintf('Window: %s width [%d] is beyond our parent\'s width [%d].',$name,$width,$this->parent->width));
if (($x > $this->parent->bx) || ($x+$width-1 > $this->parent->bx))
throw new \Exception(sprintf('Window: %s start x [%d] and width [%d] is beyond our parent\'s end x [%d].',$name,$x,$width,$this->parent->bx));
if (($this->y < 1) || ($height > $this->parent->height))
throw new \Exception(sprintf('Window: %s height [%d] is beyond our parent\'s height [%d].',$name,$height,$this->parent->height));
if (($y > $this->parent->by) || ($y+$height-1 > $this->parent->by))
throw new \Exception(sprintf('Window: %s start y [%d] and height [%d] is beyond our parent\'s end y [%s].',$name,$y,$height,$this->parent->by));
} elseif ($parent instanceof Page) {
$this->parent = $parent;
}
$this->width = $this->canvaswidth = $width;
$this->height = $this->canvasheight = $height;
if ($debug) {
$this->canvaswidth = $width*2;
$this->canvasheight = $height*2;
}
// Fill with data
for($y=1;$y<=$this->canvasheight;$y++) {
for($x=1;$x<=$this->canvaswidth;$x++) {
if (! isset($this->content[$y]))
$this->content[$y] = [];
$this->content[$y][$x] = $debug
? new Char((($x > $this->width) || ($y > $this->height)) ? strtoupper($this->name[0]) : strtolower($this->name[0]))
: new Char();
}
}
}
public function __get($key): mixed
{
switch ($key) {
case 'bx': return $this->x+$this->width-1;
case 'by': return $this->y+$this->height-1;
case 'checkbounds': return $this->checkbounds;
case 'child':
return $this->child->sort(function($a,$b) {return ($a->z < $b->z) ? -1 : (($b->z < $a->z) ? 1 : 0); });
case 'name':
return $this->name;
case 'height':
case 'parent':
case 'visible':
case 'width':
case 'x':
case 'y':
case 'z':
return $this->{$key};
default:
throw new \Exception('Unknown key: '.$key);
}
}
public function __set($key,$value): void
{
switch ($key) {
case 'child':
if ($value instanceof self)
$this->child->push($value);
else
throw new \Exception('child not an instance of Window()');
break;
case 'content':
$this->content = $value;
break;
case 'parent':
if ($this->parent)
throw new \Exception('parent already DEFINED');
else
$this->parent = $value;
break;
case 'visible':
$this->visible = $value;
break;
default:
throw new \Exception('Unknown key: '.$key);
}
}
/**
* Build this window, returning an array of Char that will be rendered by Page
*
* @param int $xoffset - (int) This windows x position for its parent
* @param int $yoffset - (int) This windows y position for its parent
* @param bool $debug - (int) debug mode, which fills the window with debug content
* @return array
*/
public function build(int $xoffset,int $yoffset,bool $debug=FALSE): array
{
$display = [];
if ($debug) {
dump('********* ['.$this->name.'] *********');
dump('name :'.$this->name);
dump('xoff :'.$xoffset);
dump('yoff :'.$yoffset);
dump('x :'.$this->x);
dump('bx :'.$this->bx);
dump('ox :'.$this->ox);
dump('y :'.$this->y);
dump('by :'.$this->by);
dump('oy :'.$this->oy);
dump('lines :'.count(array_keys($this->content)));
//dump('content:'.join('',$this->content[1]));
}
if ($debug)
dump('-------------');
for ($y=1;$y<=$this->height;$y++) {
if ($debug)
echo sprintf('%02d',$y).':';
$sy = $this->y-1+$y+$yoffset-1;
for ($x=1;$x<=$this->width;$x++) {
if ($debug)
dump('- Checking :'.$this->name.', y:'.($y+$this->oy).', x:'.($x+$this->ox));
$sx = $this->x-1+$x+$xoffset-1;
if (! isset($display[$sy]))
$display[$sy] = [];
if (isset($this->content[$y+$this->oy]) && isset($this->content[$y+$this->oy][$x+$this->ox])) {
$display[$sy][$sx] = $this->content[$y+$this->oy][$x+$this->ox];
if ($debug)
dump('- storing in y:'.($sy).', x:'.($sx).', ch:'.$display[$sy][$sx]->ch);
} else {
$display[$sy][$sx] = new Char();
if ($debug)
dump('- nothing for y:'.($sy).', x:'.($sx).', ch:'.$display[$sy][$sx]->ch);
}
}
if ($debug)
dump('---');
}
if ($debug)
dump('----LOOKING AT CHILDREN NOW---------');
if ($debug) {
dump('Window:'.$this->name.', has ['.$this->child->filter(function($child) { return $child->visible; })->count().'] children');
$this->child->each(function($child) {
dump(' - child:'.$child->name.', visible:'.$child->visible);
});
}
// Fill the array with our values
foreach ($this->child->filter(function($child) { return $child->visible; }) as $child) {
if ($debug) {
dump('=========== ['.$child->name.'] =============');
dump('xoff :'.$xoffset);
dump('yoff :'.$yoffset);
dump('x :'.$this->x);
dump('y :'.$this->y);
}
$draw = $child->build($this->x+$xoffset-1,$this->y+$yoffset-1,$debug);
if ($debug)
dump('draw y:'.join(',',array_keys($draw)));
foreach (array_keys($draw) as $y) {
foreach (array_keys($draw[$y]) as $x) {
if (! isset($display[$y]))
$display[$y] = [];
$display[$y][$x] = $draw[$y][$x];
}
}
if ($debug) {
//dump('draw 1:'.join(',',array_keys($draw[1])));
dump('=========== END ['.$child->name.'] =============');
}
}
if ($debug) {
dump('this->name:'.$this->name);
dump('this->y:'.$this->y);
dump('display now:'.join(',',array_values($display[$this->y])));
dump('********* END ['.$this->name.'] *********');
foreach ($display as $y => $data) {
dump(sprintf("%02d:%s (%d)\r\n",$y,join('',$data),count($data)));
}
}
return $display;
}
public function xdebug(string $text) {
return '- '.$text.': '.$this->name.'('.$this->x.'->'.($this->bx).') width:'.$this->width.' ['.$this->y.'=>'.$this->by.'] with z:'.$this->z;
}
/**
* Render this window
*
* @param $start - (int) Starting x position
* @param $end - (int) Ending x position
* @param $y - (int) Line to render
* @param $color - (bool) Whether to include color
* @returns {{x: number, content: string}}
*/
public function xdraw($start,$end,$y,$color): array
{
$content = '';
for ($x=$start;$x<=$end;$x++) {
$rx = $this->ox+$x;
$ry = $this->oy+$y;
// Check if we have an attribute to draw
if (! (isset($this->content[$ry])) || ! (isset($this->content[$ry][$rx]))) {
$content += ' ';
continue;
}
if ($color === NULL || $color === true) {
// Only write a new attribute if it has changed
if (($this->last === NULL) || ($this->last !== $this->content[$ry][$rx]->attr)) {
$this->last = $this->content[$ry][$rx]->attr;
$content += ($this->last === null ? BG_BLACK|LIGHTGRAY : $this->last);
}
}
try {
$content += ($this->content[$ry][$rx]->ch !== null ? $this->content[$ry][$rx]->ch : ' ');
} catch (\Exception $e) {
dump($e);
dump('---');
dump('x:'.($x-$this->x));
dump('y:'.($y-$this->y));
dump('ox:'.$this->ox);
dump('oy:'.$this->oy);
dump('$rx:'.$rx);
dump('$ry:'.$ry);
exit();
}
}
return ['content'=>$content, 'x'=>$end - $start + 1];
}
}

View File

@@ -9,7 +9,6 @@ use Illuminate\Support\Facades\Log;
use App\Classes\Dynamic;
use App\Models\Address;
use App\Traits\HubStats as HubStatsTrait;
/**
* This method will generate the hub status for an upstream Host/RC/ZC
@@ -19,8 +18,6 @@ use App\Traits\HubStats as HubStatsTrait;
*/
class HubStats extends Dynamic
{
use HubStatsTrait;
private const LOGKEY = 'DHS';
private string $name = '';
@@ -35,8 +32,41 @@ class HubStats extends Dynamic
{
$date = Carbon::now()->yesterday()->endOfday();
$r = $this->HubStats($date)
->where('zones.id',$this->ao->zone_id);
$r = Address::select([
'a.id',
'addresses.system_id',
'addresses.zone_id',
'addresses.region_id',
'addresses.host_id',
'addresses.node_id',
'addresses.point_id',
'addresses.hub_id',
'addresses.role',
DB::raw('sum(a.uncollected_echomail) as uncollected_echomail'),
DB::raw('sum(a.uncollected_netmail) as uncollected_netmail'),
DB::raw('sum(a.uncollected_files) as uncollected_files')
])
->from(
Address::UncollectedEchomailTotal()
->where('echomails.created_at','<',$date)
->union(Address::UncollectedNetmailTotal()
->where('netmails.created_at','<',$date)
)
->union(Address::UncollectedFilesTotal()
->where('files.created_at','<',$date)
),'a')
->where('systems.active',true)
->where('addresses.active',TRUE)
->where('zones.active',TRUE)
->where('domains.active',TRUE)
->where('zones.id',$this->ao->zone_id)
->join('addresses',['addresses.id'=>'a.id'])
->join('systems',['systems.id'=>'addresses.system_id'])
->join('zones',['zones.id'=>'addresses.zone_id'])
->join('domains',['domains.id'=>'zones.domain_id'])
->ftnOrder()
->groupBy('addresses.system_id','a.id','addresses.zone_id','addresses.region_id','addresses.host_id','addresses.node_id','addresses.point_id','addresses.hub_id','addresses.role')
->with(['system','zone.domain']);
$header = "| %-12s | %4d | %3d | %3d | %16s | %5s | %5s |\r\n";
@@ -70,7 +100,7 @@ class HubStats extends Dynamic
$o->uncollected_echomail ?? 0,
$o->uncollected_netmail ?? 0,
$o->uncollected_files ?? 0,
$o->system->last_seen?->format('Y-m-d H:i') ?: '-',
$o->system->last_session?->format('Y-m-d H:i'),
is_null($o->system->pollmode) ? 'HOLD' : ($o->system->pollmode ? 'CRASH' : 'DAILY'),
$o->system->autohold ? 'YES' : 'NO');
}

View File

@@ -18,16 +18,20 @@ class NodelistSegment extends Dynamic
{
private const LOGKEY = 'DNL';
private ?string $name;
private string $name = '';
private Address $our_address;
private Carbon $now;
public function __construct(Address $ao,Collection $arg)
{
$this->our_address = our_address($ao->zone->domain,FALSE)->first();
$this->our_address = our_address($ao->zone->domain)->first();
$this->now = Carbon::now();
$this->name = $arg->get('name','');
$this->name = sprintf('z%dn%d.%d',
$this->our_address->zone->zone_id,
$this->our_address->host_id,
$this->now->format('z'),
);
Log::debug(sprintf('%s:- Generating Nodelist for [%s] from [%s] as [%s] with arguments',self::LOGKEY,$ao->ftn,$this->our_address->ftn,$this->our_address->role_name),['args'=>$arg]);
}
@@ -55,7 +59,7 @@ class NodelistSegment extends Dynamic
$result->push('CM');
if ($ao->system->address) {
$result->push(sprintf('INA:%s',our_address($ao->domain)->contains($ao->id) ? our_hostname($ao) : $ao->system->address));
$result->push(sprintf('INA:%s',$ao->system->address));
if (($x=$ao->system->mailers->pluck('name')->search('BINKP')) !== FALSE)
$result->push(sprintf('IBN%s',(($y=$ao->system->mailers->get($x)->pivot->port) !== 24554) ? ':'.$y : ''));
@@ -145,7 +149,7 @@ class NodelistSegment extends Dynamic
if ($oo->system_id == $so->id)
continue;
$result->push($this->entry($oo));
$result->push($this->generate($oo) ?: $this->entry($oo));
}
return $result->join("\n");
@@ -153,8 +157,6 @@ class NodelistSegment extends Dynamic
public function getName(): string
{
return ($this->name ?: sprintf('z%dn%d',
$this->our_address->zone->zone_id,
$this->our_address->host_id)).'.'.sprintf('%03d',$this->now->format('z'));
return $this->name;
}
}

View File

@@ -17,7 +17,7 @@ abstract class FTN
$this->fn,
$this->ff,
$this->fp,
).((isset($this->zone) && $this->zone) ? sprintf('@%s',$this->zone->domain->name) : '');
).($this->zone ? sprintf('@%s',$this->zone->domain->name) : '');
case 'tftn_t':
return sprintf('%d:%d/%d.%d',
@@ -25,7 +25,7 @@ abstract class FTN
$this->tn,
$this->tf,
$this->tp,
).((isset($this->zone) && $this->zone) ? sprintf('@%s',$this->zone->domain->name) : '');
).($this->zone ? sprintf('@%s',$this->zone->domain->name) : '');
case 'fftn':
return Address::findFTN($this->fftn_t);

View File

@@ -8,7 +8,6 @@ use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\Validator as ValidatorResult;
use App\Classes\FTN as FTNBase;
@@ -22,31 +21,7 @@ use App\Traits\ObjectIssetFix;
* Represents the structure of a message in a packet
*
* @note FTN packed echomail messages are ZONE agnostic.
* @note FTN packed netmails may not have an INTL kludge
*
* We work out addresses using the following approach/priority:
* = By definition we should know the author node, because it's either ours or (will be) in the nodelist (but it might not be there yet)
* = The target node may not be in the nodelist (anymore)
*
* + Echomail - only has source addresses (MUST have an AREA: tag, otherwise its netmail)
* a Origin Line " * Origin: <some text> (z:f/n.p)
* b MSGID Kludge "MSGID: z:f/n.p<@domain> <sometext>
* c net/node from msg headers (dst should be to hub to be processed)
* d domain address from packet (2.2 only) (dst should be to hub to be processed)
* e point from packet (2+/2e/2.2) (dst should be to hub to be processed)
* f zone from (2/2+/2e/2.2) (dst should be to hub to be processed)
*
* RULES:
* + if a exists, c, e, f must match
* + if b exists, c, d (if present), e, f must match
*
* + Netmail
* a INTL kludge (may not exist)
* b FMPT/TOPT (points only)
* c src & dst net/node from msg headers
* d src domain address from packet (2.2 only) (dst is to next hop, not final destination)
* e src point from packet (2+/2e/2.2) (dst is to next hop, not final destination)
* f src zone from (2/2+/2e/2.2) (dst is to next hop, not final destination)
* @package App\Classes
*/
class Message extends FTNBase
{
@@ -114,10 +89,13 @@ class Message extends FTNBase
public const AREATAG_LEN = 35; //
private array $header; // Message Header
private Collection $kludges; // TZUTC that needs to be converted to be used by Carbon @see self::kludges
private int $tzutc = 0; // TZUTC that needs to be converted to be used by Carbon @see self::kludges
private Echomail|Netmail $mo; // The object storing this packet message
private Address $us; // Our address for this message
/** @deprecated Not sure why this is needed? */
public bool $packed = FALSE; // Has the message been packed successfully
// Convert characters into printable chars
// https://int10h.org/oldschool-pc-fonts/readme/#437_charset
private const CP437 = [
@@ -166,8 +144,7 @@ class Message extends FTNBase
public static function header_len(): int
{
return collect(static::HEADER)
->sum(fn($item)=>Arr::get($item,2));
return collect(static::HEADER)->sum(function($item) { return Arr::get($item,2); });
}
/**
@@ -259,7 +236,7 @@ class Message extends FTNBase
$o->mo->from = $o->header['user_from'];
$o->mo->subject = $o->header['subject'];
$o->mo->datetime = $o->datetime->clone()->utc();
$o->mo->datetime = $o->datetime;
$o->mo->tzoffset = $o->datetime->utcOffset();
$o->mo->flags = $o->header['flags'];
$o->mo->cost = $o->header['cost'];
@@ -320,7 +297,6 @@ class Message extends FTNBase
public function __construct(Zone $zone)
{
$this->zone = $zone;
$this->kludges = collect();
}
public function __get($key)
@@ -330,11 +306,10 @@ class Message extends FTNBase
switch ($key) {
// From Addresses
// @todo $this->src no longer appears to be defined
case 'fz': return (int)Arr::get($this->src,'z');
case 'fn': return (int)($x=$this->src) ? Arr::get($x,'n') : Arr::get($this->header,'onet');
case 'ff': return (int)($x=$this->src) ? Arr::get($x,'f') : Arr::get($this->header,'onode');
case 'fp': return (int)$this->mo->kludges->get('FMPT') ?: Arr::get($this->src,'p',Arr::get($this->header,'opoint',0));
case 'fp': return (int)$this->mo->kludges->get('FMPT:') ?: Arr::get($this->src,'p',Arr::get($this->header,'opoint',0));
case 'fd': return Arr::get($this->src,'d');
case 'fzone':
@@ -351,7 +326,6 @@ class Message extends FTNBase
return Zone::where('zone_id',$this->fz)
->where('default',TRUE)
->single();
case 'fdomain':
// We'll use the zone's domain if this method class was called with a zone
if ($this->zone && (($this->zone->domain->name === Arr::get($this->src,'d')) || ! Arr::get($this->src,'d')))
@@ -369,7 +343,7 @@ class Message extends FTNBase
case 'tz': return (int)Arr::get($this->isEchomail() ? $this->src : $this->dst,'z');
case 'tn': return (int)Arr::get($this->header,'dnet');
case 'tf': return (int)Arr::get($this->header,'dnode');
case 'tp': return (int)$this->mo->kludges->get('TOPT') ?: Arr::get($this->header,'dpoint',0);
case 'tp': return (int)$this->mo->kludges->get('TOPT:') ?: Arr::get($this->header,'dpoint',0);
case 'tzone':
// Use the zone if this class was called with it.
@@ -399,6 +373,23 @@ class Message extends FTNBase
// Otherwise we'll assume the same as the source domain
return $this->fdomain ?: NULL;
case 'fftn_t':
case 'fftn':
case 'tftn_t':
case 'tftn':
return parent::__get($key);
// For 5D we need to include the domain
/* @deprecated - is this required? */
case 'fboss':
return sprintf('%d:%d/%d',$this->fz,$this->fn,$this->ff).(($x=$this->fdomain) ? '@'.$x->name : '');
case 'tboss':
return sprintf('%d:%d/%d',$this->tz,$this->tn,$this->tf).(($x=$this->tdomain) ? '@'.$x->name : '');
case 'fboss_o':
return Address::findFTN($this->fboss);
case 'tboss_o':
return Address::findFTN($this->tboss);
// Convert our message (header[datetime]) with our TZUTC into a Carbon date
case 'datetime':
try {
@@ -498,23 +489,19 @@ class Message extends FTNBase
break;
case 'tzutc':
return $this->kludges->get($key);
default:
return parent::__get($key);
throw new \Exception('Unknown key: '.$key);
}
}
public function __set(string $key,mixed $value): void
/**
* When we serialise this object, we'll need to utf8_encode some values
*
* @return array
*/
public function __serialize(): array
{
switch ($key) {
case 'tzutc':
if (! is_numeric($value))
throw new InvalidPacketException('TZUTC is not numeric '.$value);
$this->kludges->put($key,$value);
}
return $this->encode();
}
/**
@@ -525,6 +512,8 @@ class Message extends FTNBase
*/
public function __toString(): string
{
$s = Setup::findOrFail(config('app.id'));
$return = pack(collect(self::HEADER)->pluck(1)->join(''),
$this->mo->fftn->node_id, // Originating Node
$this->mo->tftn->node_id, // Destination Node
@@ -535,12 +524,15 @@ class Message extends FTNBase
$this->mo->date->format('d M y H:i:s'),
);
$return .= Str::limit($this->mo->to,self::USER_TO_LEN,'')."\00";
$return .= Str::limit($this->mo->from,self::USER_FROM_LEN,'')."\00";
$return .= Str::limit($this->mo->subject,self::SUBJECT_LEN-3)."\00";
$return .= $this->mo->to."\00";
$return .= $this->mo->from."\00";
$return .= $this->mo->subject."\00";
if (($this->mo instanceof Netmail) && $this->mo->isFlagSet(self::FLAG_LOCAL)) {
// If there isnt an INTL kludge, we'll add it
if (! $this->mo->kludges->has('INTL'))
$this->mo->kludges->put('INTL',sprintf('%s %s',$this->mo->tftn->ftn3d,$this->mo->fftn->ftn3d));
// Add our FMPT/TOPT kludges for netmails to a point
if ($this->mo instanceof Netmail) {
if ((! $this->mo->kludges->has('FMPT')) && $this->mo->fftn->point_id)
$this->mo->kludges->put('FMPT',$this->mo->fftn->point_id);
@@ -548,16 +540,12 @@ class Message extends FTNBase
$this->mo->kludges->put('TOPT',$this->mo->tftn->point_id);
}
$this->mo->kludges->put($this->mo->isFlagSet(self::FLAG_LOCAL) ? 'PID:' : 'TID:',sprintf('%s %s',Setup::PRODUCT_NAME_SHORT,Setup::version()));
$this->mo->kludges->put($this->mo->isFlagSet(self::FLAG_LOCAL) ? 'PID:' : 'TID:',sprintf('%s %s',Setup::PRODUCT_NAME_SHORT,$s->version));
$this->mo->kludges->put('DBID:',$this->mo->id);
if ($this->mo instanceof Echomail)
$return .= sprintf("AREA:%s\r",strtoupper($this->mo->echoarea->name));
// Rebuild the INTL kludge line
elseif ($this->mo instanceof Netmail)
$this->mo->kludges->put('INTL',sprintf('%s %s',$this->mo->tftn->ftn3d,$this->mo->fftn->ftn3d));
// Add some kludges
$return .= sprintf("\01TZUTC: %s\r",str_replace('+','',$this->mo->date->getOffsetString('')));
@@ -580,19 +568,14 @@ class Message extends FTNBase
$return .= sprintf("\x01Via %s @%s.UTC %s %s\r",
$this->us->ftn3d,
Carbon::now()->format('Ymd.His'),
Setup::PRODUCT_NAME_SHORT,Setup::version());
Setup::PRODUCT_NAME_SHORT,$s->version);
} else {
// FTS-0004.001/FSC-0068.001 The message SEEN-BY lines
// FTS-0004.001/FSC-0068.001 The message PATH lines
// @todo This unique() function here shouldnt be required, but is while system generated messages are storing path/seenby
$path = $this
->mo
->path
->push($this->us)
->unique('ftn')
->filter(fn($item)=>is_null($item->point_id) || ($item->point_id === 0));
$path = $this->mo->path->push($this->us)->unique('ftn')->filter(fn($item)=>($item->point_id === 0));
// Create our rogue seenby objects
$seenby = $this->mo->seenby;
@@ -606,7 +589,7 @@ class Message extends FTNBase
$seenby = $seenby
->push($this->us)
->filter(fn($item)=>is_null($item->point_id) || ($item->point_id === 0))
->filter(fn($item)=>($item->point_id === 0))
->unique('ftn')
->sortBy(function($item) { return sprintf('%05d%05d',$item->host_id,$item->node_id);});
@@ -619,6 +602,16 @@ class Message extends FTNBase
return $return;
}
/**
* When we unserialize, we'll restore (utf8_decode) some values
*
* @param array $values
*/
public function __unserialize(array $values): void
{
$this->decode($values);
}
/**
* Reduce our PATH/SEEN-BY for messages as per FSC-0068
*
@@ -679,9 +672,8 @@ class Message extends FTNBase
* @param Echomail|Netmail $o
* @return Echomail|Netmail
* @throws InvalidPacketException
* @todo Remove parsing $o as second object, make this private, and use $this->... instead of $o->...
*/
public function unpackMessage(string $message,Echomail|Netmail $o): Echomail|Netmail
private function unpackMessage(string $message,Echomail|Netmail $o): Echomail|Netmail
{
// Remove DOS \n\r
$message = preg_replace("/\n\r/","\r",$message);
@@ -690,7 +682,6 @@ class Message extends FTNBase
// First find our kludge lines
$ptr_start = 0;
try {
while (substr($message,$ptr_start,1) === "\x01") {
$ptr_end = strpos($message,"\r",$ptr_start);
@@ -706,14 +697,9 @@ class Message extends FTNBase
}
// Catch any kludges we need to process here
if (array_key_exists($m[1],self::kludges)) {
// Some earlier mystic message had a blank value for TZUTC
if ((($m[1]) === 'TZUTC:') && (! $m[2]))
$m[2] = '0000';
if (array_key_exists($m[1],self::kludges))
$this->{self::kludges[$m[1]]} = $m[2];
} else
else
$o->kludges = [$m[1],$m[2]];
}
@@ -728,7 +714,7 @@ class Message extends FTNBase
throw new InvalidPacketException('Couldnt find the end of the origin');
} elseif (! $ptr_end=strpos($message,"\r\x01",$ptr_start)) {
$ptr_end = strlen($message);
throw new InvalidPacketException('Couldnt parse the end of the content');
}
$remaining = substr($message,$ptr_end+1);
@@ -748,10 +734,10 @@ class Message extends FTNBase
$ptr_content_start = 0;
// See if we have a tagline
if ($ptr_content_end=strrpos($content,"\r... ",$ptr_content_start)) {
$o->msg = substr($content,$ptr_content_start,$ptr_content_end+1);
if ($ptr_content_end=strrpos($content,"\r\r... ",$ptr_content_start)) {
$o->msg = substr($content,$ptr_content_start,$ptr_content_end);
$ptr_content_start = $ptr_content_end+6;
$ptr_content_start = $ptr_content_end+5;
$ptr_content_end = strpos($content,"\r",$ptr_content_start);
// If there is no terminating "\r", then that's it
@@ -766,11 +752,11 @@ class Message extends FTNBase
}
// See if we have a tearline
if ($ptr_content_end=strrpos($content,"\r--- ",$ptr_content_start)) {
if ($ptr_content_end=strrpos($content,"\r\r--- ",$ptr_content_start)) {
if (! $ptr_content_start)
$o->msg = substr($content,$ptr_content_start,$ptr_content_end+1);
$o->msg = substr($content,$ptr_content_start,$ptr_content_end);
$ptr_content_start = $ptr_content_end+6;
$ptr_content_start = $ptr_content_end+5;
$ptr_content_end = strpos($content,"\r",$ptr_content_start);
// If there is no terminating "\r", then that's it
@@ -810,14 +796,9 @@ class Message extends FTNBase
$ptr_content_start = $ptr_end-$ptr_start;
}
// Trim any right \r from the message
$o->msg = rtrim($o->msg,"\r");
// Quick validation that we are done
if ($ptr_content_start !== strlen($content)) {
Log::alert(sprintf('%s:! We failed parsing the message start [%d] content [%d]',self::LOGKEY,$ptr_content_start,strlen($content)));
$o->msg = substr($message,0,$ptr_end);
}
if ($ptr_content_start !== strlen($content))
throw new InvalidPacketException('There is more data in the message content?');
}
$ptr_start = $ptr_end+1;
@@ -830,21 +811,9 @@ class Message extends FTNBase
$m = [];
preg_match('/^([^\s]+:?)+\s+(.*)$/',$line,$m);
// Messages that originate from a point dont have anything in a PATH
if (count($m) === 2)
$o->kludges = [$m[1],$m[2]];
}
} catch (\Exception $e) {
Log::error(sprintf('%s:! Error parsing message, now at offset [0x%02x] (%s)',
self::LOGKEY,
$ptr_start,
$e->getMessage()),['dump'=>hex_dump($message),'line'=>$e->getLine(),'file'=>$e->getFile()]);
throw new InvalidPacketException('Error parsing message');
}
return $o;
}
@@ -878,6 +847,9 @@ class Message extends FTNBase
'replyid' => 'sometimes|min:1',
'msg' => 'required|min:1', // @todo max message length?
'msg_crc' => 'required|size:32',
'tagline' => 'sometimes|min:1|max:255',
'tearline' => 'sometimes|min:1|max:255',
'origin' => 'sometimes|min:1|max:255',
'local' => 'sometimes|boolean',
'fftn_id' => 'required|exists:App\Models\Address,id',
'tftn_id' => $this->isNetmail() ? 'required|exists:App\Models\Address,id' : 'prohibited',
@@ -898,26 +870,13 @@ class Message extends FTNBase
);
$validator->after(function($validator) {
// @todo If the message has an INTL kludge, we send the message to our ZC, and if we are the ZC onto the dst ZC
// @todo So validate we can send it on
// @todo This validation below is incorrect - netmails only need to have an INTL if they are traversing zones
// @todo Without an INTL it is affecting our determination of a source zone/dst zone
if (($this->mo instanceof Netmail) && (! $this->mo->kludges->has('INTL')))
$validator->errors()->add('no-intl','Netmail message is missing INTL KLUDGE.');
if ($this->zone->domain->flatten) {
if (! $this->zone->domain->zones->pluck('zone_id')->contains($this->fz))
$validator->errors()->add('invalid-zone',sprintf('Message from zone [%d] doesnt match any zone in domain for packet zone [%d].',$this->fz,$this->zone->zone_id));
if (! $this->zone->domain->zones->pluck('zone_id')->contains($this->tz))
$validator->errors()->add('invalid-zone',sprintf('Message to zone [%d] doesnt match any zone in domain for packet zone [%d].',$this->fz,$this->zone->zone_id));
$validator->errors()->add('invalid-zone',sprintf('Message zone [%d] doesnt match any zone in domain for packet zone [%d].',$this->fz,$this->zone->zone_id));
} else {
if ($this->zone->zone_id !== $this->fz)
$validator->errors()->add('invalid-zone',sprintf('Message from zone [%d] doesnt match packet zone [%d].',$this->fz,$this->zone->zone_id));
if ($this->zone->zone_id !== $this->tz)
$validator->errors()->add('invalid-zone',sprintf('Message to zone [%d] doesnt match packet zone [%d].',$this->tz,$this->zone->zone_id));
$validator->errors()->add('invalid-zone',sprintf('Message zone [%d] doesnt match packet zone [%d].',$this->fz,$this->zone->zone_id));
}
if (! $this->fftn)

View File

@@ -3,7 +3,6 @@
namespace App\Classes\FTN;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
@@ -12,8 +11,8 @@ use Symfony\Component\HttpFoundation\File\File;
use App\Classes\FTN as FTNBase;
use App\Exceptions\InvalidPacketException;
use App\Models\{Address,Echomail,Netmail,Software,System,Zone};
use App\Notifications\Netmails\{EchomailBadAddress,NetmailBadAddress};
use App\Models\{Address,Domain,Echomail,Netmail,Software,System,Zone};
use App\Notifications\Netmails\EchomailBadAddress;
/**
* Represents a Fidonet Packet, that contains an array of messages.
@@ -52,7 +51,6 @@ abstract class Packet extends FTNBase implements \Iterator, \Countable
protected Address $fftn_p; // Address the packet is from (when packing messages)
protected Address $tftn_p; // Address the packet is to (when packing messages)
protected Collection $messages; // Messages in the Packet
protected string $content; // Outgoing packet data
public Collection $errors; // Messages that fail validation
protected int $index; // Our array index
protected $pass_p = NULL; // Overwrite the packet password (when packing messages)
@@ -68,7 +66,7 @@ abstract class Packet extends FTNBase implements \Iterator, \Countable
* @return bool
*/
abstract public static function is_type(string $header): bool;
abstract protected function header(Collection $msgs): string;
abstract protected function header(): string;
/* STATIC */
@@ -88,12 +86,11 @@ abstract class Packet extends FTNBase implements \Iterator, \Countable
* @param mixed $f File handler returning packet data
* @param string $name
* @param int $size
* @param System|null $so - The system that sent us the packet, used to figure out domains if the packet is for a different zone
* @param bool $process
* @param Domain|null $domain
* @return Packet
* @throws InvalidPacketException
*/
public static function process(mixed $f,string $name,int $size,System $so=NULL,bool $process=TRUE): self
public static function process(mixed $f,string $name,int $size,Domain $domain=NULL): self
{
Log::debug(sprintf('%s:+ Opening Packet [%s] with size [%d]',self::LOGKEY,$name,$size));
@@ -140,40 +137,25 @@ abstract class Packet extends FTNBase implements \Iterator, \Countable
} else
throw new InvalidPacketException('Not a valid packet, not EOP or SOM:'.bin2hex($x));
Log::info(sprintf('%s:- Packet [%s] is a [%s] packet',self::LOGKEY,$o->name,get_class($o)));
Log::info(sprintf('%s:- Packet [%s] is a [%s] packet, dated [%s]',self::LOGKEY,$o->name,get_class($o),$o->date));
if ($o->fz && $o->fd) {
$o->zone = Zone::where('zone_id',$o->fz)
// Work out the packet zone
if ($o->fz && ($o->fd || $domain)) {
$o->zone = Zone::select('zones.*')
->join('domains',['domains.id'=>'zones.domain_id'])
->where('name',$o->fd)
->where('zone_id',$o->fz)
->where('name',$o->fd ?: $domain->name)
->single();
} elseif ($o->fz && $so) {
Log::alert(sprintf('%s:! No domain in the packet, work it out from the system [%d] for zone [%d]',self::LOGKEY,$so->name,$o->fz));
if (($x=$so->zones->where('zone_id',$o->fz)->unique('domain_id'))->count() === 1) {
$o->zone = $x->pop();
} else {
Log::alert(sprintf('%s:! Node [%s] has two zones with [%d]',self::LOGKEY,$so->name,$o->fz));
}
}
// If zone is not set, then we need to use a default zone - the messages may not be from this zone.
if (empty($o->zone)) {
Log::alert(sprintf('%s:! We couldnt work out the packet zone, so we have fallen back to the default for [%d]',self::LOGKEY,$o->fz));
try {
$o->zone = Zone::where('zone_id',$o->fz)
->where('default',TRUE)
->sole();
} catch (ModelNotFoundException $e) {
throw new InvalidPacketException(sprintf('%s:! We couldnt work out the packet zone, and there isnt a default for [%d]',self::LOGKEY,$o->fz));
->singleOrFail();
}
}
Log::info(sprintf('%s:- Packet Dated [%s] from [%s] to [%s]',self::LOGKEY,$o->date,$o->fftn_t,$o->tftn_t));
$message = ''; // Current message we are building
$msgbuf = '';
@@ -189,8 +171,7 @@ abstract class Packet extends FTNBase implements \Iterator, \Countable
|| (($end=strpos($msgbuf,"\x00".self::PACKED_END,$leader)) !== FALSE))
{
// Parse our message
Log::debug(sprintf('%s:- Message at offset [%d] in [%s]',self::LOGKEY,$read_ptr-strlen($readbuf),$name));
$o->parseMessage(substr($msgbuf,0,$end),$process);
$o->parseMessage(substr($msgbuf,0,$end));
$msgbuf = substr($msgbuf,$end+3);
continue;
@@ -201,7 +182,7 @@ abstract class Packet extends FTNBase implements \Iterator, \Countable
}
// If we get here
throw new InvalidPacketException(sprintf('Cannot determine END of message/packet: %s|%s',get_class($o),hex_dump($message)));
throw new InvalidPacketException(sprintf('Cannot determine END of message/packet: %s|%s',get_class($o),hex_dump($message)));;
}
if ($msgbuf)
@@ -269,7 +250,7 @@ abstract class Packet extends FTNBase implements \Iterator, \Countable
case 'software':
Software::unguard();
$o = Software::firstOrNew(['code'=>$this->product,'type'=>Software::SOFTWARE_TOSSER]);
$o = Software::singleOrNew(['code'=>$this->product,'type'=>Software::SOFTWARE_TOSSER]);
Software::reguard();
return $o;
@@ -305,7 +286,20 @@ abstract class Packet extends FTNBase implements \Iterator, \Countable
*/
public function __toString(): string
{
return $this->content;
if (empty($this->messages))
throw new InvalidPacketException('Refusing to make an empty packet');
if (empty($this->tftn_p) || empty($this->fftn_p))
throw new InvalidPacketException('Cannot generate a packet without a destination address');
$return = $this->header();
foreach ($this->messages as $o)
$return .= self::PACKED_MSG_LEAD.$o->packet($this->tftn_p);
$return .= "\00\00";
return $return;
}
/* INTERFACE */
@@ -345,6 +339,53 @@ abstract class Packet extends FTNBase implements \Iterator, \Countable
/* METHODS */
/**
* When creating a new packet, set the header.
*
* @param Address $oo
* @param Address $o
* @param string|null $passwd Override the password used in the packet
* @deprecated Use Packet::generate(), which should generate a packet of the right type
*/
public function addressHeader(Address $oo,Address $o,string $passwd=NULL): void
{
Log::debug(sprintf('%s:+ Creating packet for [%s]',self::LOGKEY,$o->ftn));
$date = Carbon::now();
// Create Header
$this->header = [
'ozone' => $oo->zone->zone_id, // Orig Zone
'dzone' => $o->zone->zone_id, // Dest Zone
'onet' => $oo->host_id ?: $oo->region_id, // Orig Net
'dnet' => $o->host_id ?: $o->region_id, // Dest Net
'onode' => $oo->node_id, // Orig Node
'dnode' => $o->node_id, // Dest Node
'opoint' => $oo->point_id, // Orig Point
'dpoint' => $o->point_id, // Dest Point
'odomain' => $oo->zone->domain->name, // Orig Domain
'ddomain' => $o->zone->domain->name, // Dest Domain
'y' => $date->format('Y'), // Year
'm' => $date->format('m')-1, // Month
'd' => $date->format('d'), // Day
'H' => $date->format('H'), // Hour
'M' => $date->format('i'), // Minute
'S' => $date->format('s'), // Second
'password' => strtoupper((! is_null($passwd)) ? $passwd : $o->session('pktpass')), // Packet Password
];
}
/**
* Add a message to this packet
*
* @param Message $o
* @deprecated No longer used when Address::class is updated
*/
public function addMail(Message $o): void
{
$this->messages->push($o);
}
public function for(Address $ao): self
{
$this->tftn_p = $ao;
@@ -365,20 +406,7 @@ abstract class Packet extends FTNBase implements \Iterator, \Countable
public function mail(Collection $msgs): self
{
if (! $msgs->count())
throw new InvalidPacketException('Refusing to make an empty packet');
if (empty($this->tftn_p) || empty($this->fftn_p))
throw new InvalidPacketException('Cannot generate a packet without a destination address');
$this->content = $this->header($msgs);
foreach ($msgs as $o)
$this->content .= self::PACKED_MSG_LEAD.$o->packet($this->tftn_p);
$this->content .= "\00\00";
$this->messages = $msgs->map(fn($item)=>$item->only(['id','datetime']));
$this->messages = $msgs;
return $this;
}
@@ -387,25 +415,25 @@ abstract class Packet extends FTNBase implements \Iterator, \Countable
* Parse a message in a mail packet
*
* @param string $message
* @param bool $process
* @throws \Exception
* @throws InvalidPacketException|\Exception
*/
private function parseMessage(string $message,bool $process): void
private function parseMessage(string $message): void
{
Log::info(sprintf('%s:+ Processing packet message [%d] bytes',self::LOGKEY,strlen($message)));
$msg = Message::parseMessage($message,$this->zone);
// If the message is invalid, we'll ignore it
if ($process && $msg->errors->count()) {
if ($msg->errors->count()) {
Log::info(sprintf('%s:- Message [%s] has [%d] errors',self::LOGKEY,$msg->msgid ?: 'No ID',$msg->errors->count()));
// If the messages is not for the right zone, we'll ignore it
if ($msg->errors->has('invalid-zone')) {
Log::alert(sprintf('%s:! Message [%s] is from|to an invalid zone [%s|%s], packet is from [%s] - ignoring it',self::LOGKEY,$msg->msgid,$msg->get_fftn,$msg->get_tftn,$this->fz));
Log::alert(sprintf('%s:! Message [%s] is from an invalid zone [%s], packet is from [%s] - ignoring it',self::LOGKEY,$msg->msgid,$msg->fftn->zone->zone_id,$this->fftn->zone->zone_id));
if (! $msg->kludges->get('RESCANNED'))
Notification::route('netmail',$this->fftn)->notify(($msg instanceof Echomail) ? new EchomailBadAddress($msg) : new NetmailBadAddress($msg));
Notification::route('netmail',$this->fftn)->notify(new EchomailBadAddress($msg));
return;
}
@@ -414,7 +442,7 @@ abstract class Packet extends FTNBase implements \Iterator, \Countable
if ($msg->errors->has('from') && $this->fftn && $this->fftn->zone_id) {
Log::debug(sprintf('%s:^ From address [%s] doesnt exist, it needs to be created',self::LOGKEY,$msg->set->get('set_fftn')));
$ao = Address::findFTN($msg->set->get('set_fftn'),TRUE,TRUE);
$ao = Address::findFTN($msg->set->get('set_fftn'),TRUE);
if ($ao?->exists && ($ao->zone?->domain_id !== $this->fftn->zone->domain_id)) {
Log::alert(sprintf('%s:! From address [%s] domain [%d] doesnt match packet domain [%d]?',self::LOGKEY,$msg->set->get('set_fftn'),$ao->zone?->domain_id,$this->fftn->zone->domain_id));
@@ -425,38 +453,18 @@ abstract class Packet extends FTNBase implements \Iterator, \Countable
if (! $ao) {
$so = System::createUnknownSystem();
$ao = Address::createFTN($msg->set->get('set_fftn'),$so);
Log::alert(sprintf('%s:- From FTN [%s] is not defined, created new entry for (%d)',self::LOGKEY,$msg->set->get('set_fftn'),$ao->id));
}
$msg->fftn_id = $ao->id;
$msg->errors->forget('from');
$msg->errors->forget('fftn_id');
Log::alert(sprintf('%s:- From FTN [%s] is not defined, created new entry for (%d)',self::LOGKEY,$msg->set->get('set_fftn'),$ao->id));
}
// If the $msg->tftn doesnt exist, we'll need to create it
if ($msg->errors->has('to') && $this->tftn && $this->tftn->zone_id) {
$ao = Address::findFTN($msg->set->get('set_tftn'),TRUE,TRUE);
// If this is a netmail message, to a non existant address, we need to bounce it
if (($msg instanceof Netmail)) {
if ((! $ao) && our_address()->contains(Address::newFTN($msg->set_tftn)?->parent())) {
Log::alert(sprintf('%s:^ To address [%s] doesnt exist, netmail will be bounced',self::LOGKEY,$msg->set->get('set_tftn')));
$this->messages->push($msg);
return;
// If this is a netmail message, to a non existant address, we need to bounce it
} elseif ($ao && (! $ao->active) && our_address()->contains($ao->parent())) {
Log::alert(sprintf('%s:^ To address [%s] isnt active, netmail will be bounced',self::LOGKEY,$msg->set->get('set_tftn')));
$this->messages->push($msg);
return;
}
}
Log::debug(sprintf('%s:^ To address [%s] doesnt exist, it needs to be created',self::LOGKEY,$msg->set->get('set_tftn')));
$ao = Address::findFTN($msg->set->get('set_tftn'),TRUE);
if ($ao?->exists && ($ao->zone?->domain_id !== $this->tftn->zone->domain_id)) {
Log::alert(sprintf('%s:! To address [%s] domain [%d] doesnt match packet domain [%d]?',self::LOGKEY,$msg->set->get('set_tftn'),$ao->zone?->domain_id,$this->fftn->zone->domain_id));
@@ -465,14 +473,11 @@ abstract class Packet extends FTNBase implements \Iterator, \Countable
if (! $ao) {
$so = System::createUnknownSystem();
$ao = Address::createFTN($msg->set->get('set_tftn'),$so);
Log::alert(sprintf('%s:- To FTN [%s] is not defined, created new entry for (%d)',self::LOGKEY,$msg->set->get('set_tftn'),$ao->id));
$ao = Address::createFTN($msg->set->get('set_fftn'),$so);
}
$msg->tftn_id = $ao->id;
$msg->errors->forget('to');
$msg->errors->forget('tftn_id');
Log::alert(sprintf('%s:- To FTN [%s] is not defined, created new entry for (%d)',self::LOGKEY,$msg->set->get('set_tftn'),$ao->id));
}
// If there is no fftn, then its from a system that we dont know about
@@ -485,7 +490,7 @@ abstract class Packet extends FTNBase implements \Iterator, \Countable
}
// @todo If the message from domain (eg: $msg->fftn->zone->domain) is different to the packet address domain ($pkt->fftn->zone->domain), we'll skip this message
//Log::debug(sprintf('%s:^ Message [%s] - Packet from domain [%d], Message domain [%d]',self::LOGKEY,$msg->msgid,$this->fftn->zone->domain_id,$msg->fftn->zone->domain_id));
Log::debug(sprintf('%s:^ Message [%s] - Packet from domain [%d], Message domain [%d]',self::LOGKEY,$msg->msgid,$this->fftn->zone->domain_id,$msg->fftn->zone->domain_id));
$this->messages->push($msg);
}
@@ -499,8 +504,15 @@ abstract class Packet extends FTNBase implements \Iterator, \Countable
public function password(string $password=NULL): self
{
if ($password && (strlen($password) < 9))
$this->pass_p = strtoupper($password);
$this->pass_p = $password;
return $this;
}
/** @deprecated Is this used? */
public function pluck(string $key): Collection
{
throw new \Exception(sprintf('%s:! This function is deprecated - [%s]',self::LOGKEY,$key));
return $this->messages->pluck($key);
}
}

View File

@@ -3,7 +3,6 @@
namespace App\Classes\FTN\Packet;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use App\Classes\FTN\Packet;
use App\Models\Setup;
@@ -63,10 +62,11 @@ final class FSC39 extends Packet
/**
* Create our message packet header
*/
protected function header(Collection $msgs): string
protected function header(): string
{
$oldest = $this->messages->sortBy('date')->last();
$oldest = $this->messages->sortBy('datetime')->last();
try {
return pack(collect(self::HEADER)->pluck(1)->join(''),
$this->fftn_p->node_id, // Orig Node
$this->tftn_p->node_id, // Dest Node
@@ -82,7 +82,7 @@ final class FSC39 extends Packet
$this->tftn_p->host_id, // Dest Net
(Setup::PRODUCT_ID & 0xff), // Product Code Lo
Setup::PRODUCT_VERSION_MAJ, // Product Version Major
$this->pass_p ?: $this->tftn_p->pass_packet, // Packet Password
$this->pass_p ?: $this->tftn_p->session('pktpass'), // Packet Password
$this->fftn_p->zone->zone_id, // Orig Zone
$this->tftn_p->zone->zone_id, // Dest Zone
'', // Reserved
@@ -96,6 +96,10 @@ final class FSC39 extends Packet
$this->tftn_p->point_id, // Dest Point
strtoupper(hexstr(Setup::PRODUCT_ID)), // ProdData
);
} catch (\Exception $e) {
return $e->getMessage();
}
}
/**

View File

@@ -3,7 +3,6 @@
namespace App\Classes\FTN\Packet;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use App\Classes\FTN\Packet;
use App\Models\Setup;
@@ -55,8 +54,9 @@ final class FSC45 extends Packet
/**
* Create our message packet header
*/
protected function header(Collection $msgs): string
protected function header(): string
{
try {
return pack(collect(self::HEADER)->pluck(1)->join(''),
$this->fftn_p->node_id, // Orig Node
$this->tftn_p->node_id, // Dest Node
@@ -69,13 +69,17 @@ final class FSC45 extends Packet
$this->tftn_p->host_id, // Dest Net
(Setup::PRODUCT_ID & 0xff), // Product Code
Setup::PRODUCT_VERSION_MAJ, // Product Version
$this->pass_p ?: $this->tftn_p->pass_packet, // Packet Password
$this->pass_p ?: $this->tftn_p->session('pktpass'), // Packet Password
$this->fftn_p->zone->zone_id, // Orig Zone
$this->tftn_p->zone->zone_id, // Dest Zone
$this->fftn_p->zone->domain->name, // Orig Domain
$this->tftn_p->zone->domain->name, // Dest Domain
strtoupper(hexstr(Setup::PRODUCT_ID)), // ProdData
);
} catch (\Exception $e) {
return $e->getMessage();
}
}
/**

View File

@@ -3,7 +3,6 @@
namespace App\Classes\FTN\Packet;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use App\Classes\FTN\Packet;
use App\Models\Setup;
@@ -55,10 +54,6 @@ final class FSC48 extends Packet
case 'capability':
return sprintf('%016b',Arr::get($this->header,'capword'));
case 'fn':
// If the packet is from a point, then onet will be 0xffff
return ($x=Arr::get($this->header,'onet')) === 0xffff ? Arr::get($this->header,'auxnet') : $x;
default:
return parent::__get($key);
}
@@ -67,10 +62,11 @@ final class FSC48 extends Packet
/**
* Create our message packet header
*/
protected function header(Collection $msgs): string
protected function header(): string
{
$oldest = $msgs->sortBy('date')->last();
$oldest = $this->messages->sortBy('datetime')->last();
try {
return pack(collect(self::HEADER)->pluck(1)->join(''),
$this->fftn_p->node_id, // Orig Node
$this->tftn_p->node_id, // Dest Node
@@ -86,7 +82,7 @@ final class FSC48 extends Packet
$this->tftn_p->host_id, // Dest Net
(Setup::PRODUCT_ID & 0xff), // Product Code Lo
Setup::PRODUCT_VERSION_MAJ, // Product Version Major
$this->pass_p ?: $this->tftn_p->pass_packet, // Packet Password
$this->pass_p ?: $this->tftn_p->session('pktpass'), // Packet Password
$this->fftn_p->zone->zone_id, // Orig Zone
$this->tftn_p->zone->zone_id, // Dest Zone
$this->fftn_p->point_id ? $this->fftn_p->host_id : 0x00, // Aux Net
@@ -100,6 +96,10 @@ final class FSC48 extends Packet
$this->tftn_p->point_id, // Dest Point
strtoupper(hexstr(Setup::PRODUCT_ID)), // ProdData
);
} catch (\Exception $e) {
return $e->getMessage();
}
}
/**

View File

@@ -3,7 +3,6 @@
namespace App\Classes\FTN\Packet;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use App\Classes\FTN\Packet;
use App\Models\Setup;
@@ -56,10 +55,11 @@ final class FTS1 extends Packet
/**
* Create our message packet header
*/
protected function header(Collection $msgs): string
protected function header(): string
{
$oldest = $this->messages->sortBy('datetime')->last();
try {
return pack(collect(self::HEADER)->pluck(1)->join(''),
$this->fftn_p->node_id, // Orig Node
$this->tftn_p->node_id, // Dest Node
@@ -75,11 +75,15 @@ final class FTS1 extends Packet
$this->tftn_p->host_id, // Dest Net
(Setup::PRODUCT_ID & 0xff), // Product Code Lo
Setup::PRODUCT_VERSION_MAJ, // Product Version Major
$this->pass_p ?: $this->tftn_p->pass_packet, // Packet Password
$this->pass_p ?: $this->tftn_p->session('pktpass'), // Packet Password
$this->fftn_p->zone->zone_id, // Orig Zone
$this->tftn_p->zone->zone_id, // Dest Zone
'', // Reserved
);
} catch (\Exception $e) {
return $e->getMessage();
}
}
/**

View File

@@ -11,7 +11,7 @@ abstract class Process
{
public static function canProcess(Echoarea $eao): bool
{
return (bool)$eao->automsgs;
return $eao->automsgs ? TRUE : FALSE;
}
/**

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Classes\FTN\Process\Netmail;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Log;
use App\Classes\FTN\Process;
use App\Models\{Echomail,Netmail};
use App\Notifications\Netmails\Areafix as AreafixNotification;
use App\Notifications\Netmails\Areafix\NotConfiguredHere as AreafixNotConfiguredHereNotification;
/**
* Process messages to Ping
*
* @package App\Classes\FTN\Process
*/
final class Areafix extends Process
{
private const LOGKEY = 'RP-';
public static function handle(Echomail|Netmail $mo): bool
{
if (strtolower($mo->to) !== 'areafix')
return FALSE;
Log::info(sprintf('%s:- Processing AREAFIX message from (%s) [%s]',self::LOGKEY,$mo->from,$mo->fftn));
// If this is not a node we manage, then respond with a sorry can help you
if ($mo->fftn->system->sessions->count())
Notification::route('netmail',$mo->fftn)->notify(new AreafixNotification($mo));
else
Notification::route('netmail',$mo->fftn)->notify(new AreafixNotConfiguredHereNotification($mo));
return TRUE;
}
}

View File

@@ -1,50 +0,0 @@
<?php
namespace App\Classes\FTN\Process\Netmail;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Log;
use App\Classes\FTN\Process;
use App\Models\{Echomail,Netmail};
use App\Notifications\Netmails\Areafix\{InvalidPassword,NotConfiguredHere};
/**
* Process messages to Ping
*
* @package App\Classes\FTN\Process
*/
abstract class Robot extends Process
{
private const LOGKEY = 'RPR';
public static function handle(Echomail|Netmail $mo): bool
{
if (((strtolower($mo->to) !== 'areafix') && (strtolower($mo->to) !== 'filefix')) || (! ($mo instanceof Netmail)))
return FALSE;
Log::info(sprintf('%s:- Processing *FIX [%s] message from (%s) [%s]',self::LOGKEY,$mo->to,$mo->from,$mo->fftn->ftn));
// If this is not a node we manage, then respond with a sorry can help you
if (! $mo->fftn->system->sessions->count()) {
Notification::route('netmail',$mo->fftn)->notify(new NotConfiguredHere($mo));
return TRUE;
}
// If this nodes password is not correct
if ($mo->fftn->pass_fix !== strtoupper($mo->subject)) {
Notification::route('netmail',$mo->fftn)->notify(new InvalidPassword($mo));
return TRUE;
}
if ((strtolower($mo->to) === 'areafix'))
return static::areafix($mo);
if ((strtolower($mo->to) === 'filefix'))
return static::filefix($mo);
return FALSE;
}
}

View File

@@ -1,104 +0,0 @@
<?php
namespace App\Classes\FTN\Process\Netmail\Robot;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Log;
use App\Classes\FTN\Process\Netmail\Robot;
use App\Models\{Echomail,Netmail};
use App\Notifications\Netmails\Areafix\CommandsProcessed;
/**
* Process messages to Ping
*
* @package App\Classes\FTN\Process
*/
final class Areafix extends Robot
{
private const LOGKEY = 'RPA';
public const commands = 'App\\Classes\\FTN\\Process\\Netmail\\Robot\\Areafix\\';
public static function handle(Echomail|Netmail $mo): bool
{
if ((strtolower($mo->to) !== 'areafix') || (! ($mo instanceof Netmail)))
return FALSE;
Log::info(sprintf('%s:- Processing AREAFIX [%s] message from (%s) [%s]',self::LOGKEY,$mo->to,$mo->from,$mo->fftn->ftn));
return parent::handle($mo);
}
public static function areafix(Netmail $mo): bool
{
$result = collect();
$result->push('--> BEGIN <--');
foreach ($mo->body_lines as $command) {
Log::debug(sprintf('%s:* Processing command [%s]',self::LOGKEY,$command));
// Skip empty lines
if (! $command || preg_match('/^\s+$/',$command))
continue;
$command = explode(' ',strtoupper(rtrim($command)));
Log::debug(sprintf('%s:* Processing command',self::LOGKEY),['command'=>$command]);
// If command starts with '...' or '---', its a tear/tag line, and we have reached the end
if (str_starts_with($command[0],'...') || str_starts_with($command[0],'---')) {
Log::info(sprintf('%s:= We got a tearline/tagline, end of processing',self::LOGKEY));
$result->push('--> END OF PROCESSING - TEARLINE/TAGLINE <--');
break;
// Lines starting with a space, we'll abort
} elseif (! $command[0]) {
Log::info(sprintf('%s:= Got a new line with a space, end of processing',self::LOGKEY));
$result->push('--> END OF PROCESSING - SPACE DETECTED <--');
break;
// If command doesnt start with %, its an area
} elseif (! str_starts_with($command[0],'%')) {
Log::info(sprintf('%s:= Assuming command [%s] is an AREA command',self::LOGKEY,$command[0]));
array_unshift($command,'%AREA');
}
// Some commands are reserved words
switch ($x=strtolower(substr($command[0],1))) {
case 'list':
$class = self::commands.'AreaList';
break;
default:
// Parse the message body and pluck out the commands on each line
$class = self::commands.ucfirst($x);
}
if (! class_exists($class)) {
$result->push(sprintf('%-25s <-- **COMMAND UNKNOWN**',join(' ',$command)));
Log::info(sprintf('%s:! Command UNKNOWN [%s] ',self::LOGKEY,join('|',$command)),['class'=>$class]);
continue;
}
// Drop the command from the array, the rest are arguments
array_shift($command);
// Refresh our echoareas
$mo->fftn->load('echoareas');
$o = new $class($mo,$command);
$result->push($o->process());
}
// Reply with a confirmation of what commands were processed
Notification::route('netmail',$mo->fftn)->notify(new CommandsProcessed($mo,$result));
return TRUE;
}
}

View File

@@ -1,147 +0,0 @@
<?php
namespace App\Classes\FTN\Process\Netmail\Robot\Areafix;
use Carbon\Carbon;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Jobs\AreafixRescan;
// Echoarea Processing Command
class Area extends Base
{
private const LOGKEY = 'AFA';
private const command = '%AREA';
public static function help(): array
{
return [
self::command.' [-|+]<ECHOAREA> [R|D=<DAYS>]',
' Use the area command to subscribe (+) or unsubscribe (-) to an ECHOAREA',
' Arguments:',
' - ECHOAREA (required) name of area to subscribe or unsubscribe',
' - D=DAYS (optional) number of days to resend mail from this area that you',
' havent already received (useful if you are resubscribing to an area and',
' have received mail in the past)',
' - R=DAYS (optional) number of days to resend mail from this area (even if',
' it was sent to you previously)',
' Notes:',
' * "+" is optional, and is implied if "-" is not used',
' * "R" and "D" options only apply to subscribing',
];
}
public function process(): string
{
$command = self::command.' '.join(' ',$this->arguments);
if (! ($area=Arr::get($this->arguments,0,NULL)))
return sprintf('%-25s <-- INVALID, AN AREA IS REQUIRED',$command);
// If command starts with '-', its to unsubscribe
if (str_starts_with($area,'-')) {
$sub = FALSE;
$area = substr($area,1);
} elseif (str_starts_with($area,'+')) {
$sub = TRUE;
$area = substr($area,1);
} else {
$sub = TRUE;
$area = $area;
}
Log::debug(sprintf('%s:- Processing [%s] for [%s]',self::LOGKEY,$sub ? 'ADD' : 'REMOVE',$area));
// Drop the area from the arguments, the rest are options
array_shift($this->arguments);
// Area exists
if ($ea=$this->mo->fftn->domain->echoareas->where('name',$area)->pop()) {
// If already subscribed
if ($nea=$this->mo->fftn->echoareas->where('name',$area)->pop()) {
// requesting to subscribe "You already are since..., arguments ignored
if ($sub) {
Log::debug(sprintf('%s:- FTN [%s] ALREADY subscribed to [%s] since [%s]',self::LOGKEY,$this->mo->fftn->ftn,$area,$nea->pivot->subscribed));
return sprintf('%-25s <-- ALREADY subscribed since %s',$command,$nea->pivot->subscribed);
// requesting to unsubscribe
} else {
$this->mo->fftn->echoareas()->detach($ea->id);
// Remove sub, clear queue
$x = DB::table('echomail_seenby')
->where('address_id',$this->mo->fftn->id)
->join('echomails',['echomails.id'=>'echomail_seenby.echomail_id'])
->where('echoarea_id',$nea->id)
->whereNotNull('export_at')
->whereNull('sent_at')
->orderBy('echomails.datetime')
->skip($this->mo->fftn->system->pkt_msgs) // Might already being sent in this session
->delete();
Log::debug(sprintf('%s:- FTN [%s] UNSUBSCRIBED from [%s] clearing [%s]',self::LOGKEY,$this->mo->fftn->ftn,$area,$x));
return sprintf('%-25s <-- UNSUBSCRIBED, CLEARED [%d] MSGS from queue',$command,$x);
}
// If not subscribed
} else {
// requesting to subscribe, subsubsribe and rescan if arguments
if ($sub) {
$this->mo->fftn->echoareas()->attach([$ea->id=>['subscribed'=>Carbon::now()]]);
// If we have arguments, they are to rescan
if (count($this->arguments) === 1) {
$m = [];
if (preg_match('/^([DR])=([0-9]+)$/',$this->arguments[0],$m)) {
switch ($m[1]) {
// Scan
case 'D':
AreafixRescan::dispatch($this->mo->fftn,$ea,$m[2])
->onQueue('mail');
return sprintf('%-25s <-- SUBSCRIBED, RESCAN [%d] DAYS queued',$command,$m[2]);
// Scan
case 'R':
AreafixRescan::dispatch($this->mo->fftn,$ea,$m[2],TRUE)
->onQueue('mail');
return sprintf('%-25s <-- SUBSCRIBED, FORCE RESCAN [%d] DAYS queued',$command,$m[2]);
}
}
return sprintf('%-25s <-- SUBSCRIBED, INVALID OPTIONS',$command);
} elseif (count($this->arguments) > 1) {
Log::debug(sprintf('%s:- FTN [%s] subscribed to [%s], extra commands [%s] ignored',self::LOGKEY,$this->mo->fftn->ftn,$area,join('|',$this->arguments)));
return sprintf('%-25s <-- SUBSCRIBED, OPTIONS IGNORED',$command);
} else {
Log::debug(sprintf('%s:- FTN [%s] subscribed to [%s]',self::LOGKEY,$this->mo->fftn->ftn,$area));
return sprintf('%-25s <-- SUBSCRIBED',$command);
}
// If not subscribed, "you arent subscribed, arguments ignored"
} else {
Log::debug(sprintf('%s:- FTN [%s] is NOT subscribed to [%s], NO ACTION taken',self::LOGKEY,$this->mo->fftn->ftn,$area));
return sprintf('%-25s <-- NOT subscribed, NO ACTION taken',$command);
}
}
} else {
Log::debug(sprintf('%s:- FTN [%s] area UNKNOWN [%s], NO ACTION taken',self::LOGKEY,$this->mo->fftn->ftn,$area));
return sprintf('%-25s <-- AREA UNKNOWN, NO ACTION TAKEN',$command);
}
}
}

View File

@@ -1,38 +0,0 @@
<?php
namespace App\Classes\FTN\Process\Netmail\Robot\Areafix;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
use App\Notifications\Netmails\Areafix\AreaList as AreaListNotification;
// LIST - List echoareas in a domain
class AreaList extends Base
{
private const LOGKEY = 'AFS';
private const command = '%LIST';
public static function help(): array
{
return [
self::command,
' List the available echoareas in this network',
];
}
public function process(): string
{
Log::debug(sprintf('%s:- Areafix [%s] for [%s] for [%s]',self::LOGKEY,self::command,$this->mo->fftn->ftn,join('|',$this->arguments)));
if (count($this->arguments) > 1)
return sprintf('%-25s <-- INVALID COMMAND',self::command);
else {
Notification::route('netmail',$this->mo->fftn)
->notify(new AreaListNotification($this->mo));
return sprintf('%-25s <-- COMMAND PROCESSED',self::command);
}
}
}

View File

@@ -1,26 +0,0 @@
<?php
namespace App\Classes\FTN\Process\Netmail\Robot\Areafix;
use Illuminate\Support\Facades\Log;
use App\Models\Netmail;
// Our base areafix commands class
abstract class Base
{
private const LOGKEY = 'AB-';
protected Netmail $mo;
protected array $arguments;
public function __construct(Netmail $mo,array $arguments) {
Log::debug(sprintf('%s:- Areafix [%s] command with arguments [%s] for [%s]',self::LOGKEY,get_class($this),implode('|',$arguments),$mo->fftn->ftn));
$this->mo = $mo;
$this->arguments = $arguments;
}
abstract public static function help(): array;
abstract public function process(): string;
}

View File

@@ -1,48 +0,0 @@
<?php
namespace App\Classes\FTN\Process\Netmail\Robot\Areafix;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
use App\Classes\FTN\Process\Netmail\Robot\Areafix;
use App\Notifications\Netmails\Areafix\Help as HelpNotification;
// A Help Index Command
class Help extends Base
{
private const LOGKEY = 'AFH';
private const areafix_classes = 'app/Classes/FTN/Process/Netmail/Robot/Areafix';
private const command = '%HELP';
public static function help(): array
{
return [
self::command,
' This message!',
];
}
public function process(): string
{
Log::debug(sprintf('%s:- Processing [%s] for [%s]',self::LOGKEY,self::command,$this->mo->fftn->ftn));
$result = collect();
foreach (preg_grep('/^([^.])/',scandir(self::areafix_classes)) as $file) {
if (($file === 'Base.php') || (! str_ends_with(strtolower($file),'.php')))
continue;
$class = Areafix::commands.preg_replace('/\.php$/','',$file);
if ($result->count())
$result->push('');
$result = $result
->merge($class::help());
}
Notification::route('netmail',$this->mo->fftn)->notify(new HelpNotification($this->mo,$result));
return sprintf('%-25s <-- COMMAND PROCESSED',self::command);
}
}

View File

@@ -1,65 +0,0 @@
<?php
namespace App\Classes\FTN\Process\Netmail\Robot\Areafix;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use App\Jobs\AreafixRescan;
// RESCAN - Resend echomail
class Rescan extends Base
{
private const LOGKEY = 'AFR';
private const command = '%RESCAN';
public static function help(): array
{
return [
self::command.' <ECHOAREA> [<DAYS>]',
' Use the rescan command to resend mail from an echoarea.',
' This is will resend mail again, even if you have received it in the past.',
' Arguments:',
' - ECHOAREA (required) name of area to subscribe or unsubscribe',
' - DAYS (optional) number of days to resend mail from this area that you',
' If DAYS is omitted, the default is 30.',
];
}
public function process(): string
{
Log::debug(sprintf('%s:- Areafix [%s] for [%s] for [%s]',self::LOGKEY,self::command,$this->mo->fftn->ftn,join('|',$this->arguments)));
$command = self::command.' '.join(' ',$this->arguments);
if (! ($area=Arr::get($this->arguments,0)))
return sprintf('%-25s <-- INVALID, AN AREA IS REQUIRED',$command);
if (! is_numeric($days=Arr::get($this->arguments,1,30)))
return sprintf('%-25s <-- INVALID, DAYS [%s] NOT NUMERIC',$command,$this->arguments[1]);
// Area exists
if ($ea=$this->mo->fftn->domain->echoareas->where('name',$area)->pop()) {
// If already subscribed
if ($this->mo->fftn->echoareas->pluck('name')->contains($area)) {
AreafixRescan::dispatch($this->mo->fftn,$ea,$days,TRUE)
->onQueue('mail');
Log::debug(sprintf('%s:- FTN [%s] RESCAN [%s] DAYS [%d]',self::LOGKEY,$this->mo->fftn->ftn,$area,$days));
return sprintf('%-25s <-- RESCAN [%d] DAYS queued',$command,$days);
// If not subscribed
} else {
Log::debug(sprintf('%s:- FTN [%s] is NOT subscribed to [%s], NO ACTION taken',self::LOGKEY,$this->mo->fftn->ftn,$area));
return sprintf('%-25s <-- NOT subscribed, NO ACTION taken',$command);
}
} else {
Log::debug(sprintf('%s:- FTN [%s] area UNKNOWN [%s], NO ACTION taken',self::LOGKEY,$this->mo->fftn->ftn,$area));
return sprintf('%-25s <-- AREA UNKNOWN, NO ACTION TAKEN',$command);
}
}
}

View File

@@ -1,66 +0,0 @@
<?php
namespace App\Classes\FTN\Process\Netmail\Robot\Areafix;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use App\Jobs\AreafixRescan;
// SCAN - Send unsent echomail
class Scan extends Base
{
private const LOGKEY = 'AFS';
private const command = '%SCAN';
public static function help(): array
{
return [
self::command.' <ECHOAREA> [<DAYS>]',
' Use the scan command to resend mail that you havent received yet from an',
' echoarea. This is useful if you are rejoining an echoarea, and only want',
' to get mail that you dont already have.',
' Arguments:',
' - ECHOAREA (required) name of area to subscribe or unsubscribe',
' - DAYS (optional) number of days to resend mail from this area that you',
' If DAYS is omitted, the default is 30.',
];
}
public function process(): string
{
Log::debug(sprintf('%s:- Areafix [%s] for [%s] for [%s]',self::LOGKEY,self::command,$this->mo->fftn->ftn,join('|',$this->arguments)));
$command = self::command.' '.join(' ',$this->arguments);
if (! ($area=Arr::get($this->arguments,0)))
return sprintf('%-25s <-- INVALID, AN AREA IS REQUIRED',$command);
if (! is_numeric($days=Arr::get($this->arguments,1,30)))
return sprintf('%-25s <-- INVALID, DAYS [%s] NOT NUMERIC',$command,$this->arguments[1]);
// Area exists
if ($ea=$this->mo->fftn->domain->echoareas->where('name',$area)->pop()) {
// If already subscribed
if ($this->mo->fftn->echoareas->pluck('name')->contains($area)) {
AreafixRescan::dispatch($this->mo->fftn,$ea,$days)
->onQueue('mail');
Log::debug(sprintf('%s:- FTN [%s] SCAN [%s] DAYS [%d]',self::LOGKEY,$this->mo->fftn->ftn,$area,$days));
return sprintf('%-25s <-- SCAN [%d] DAYS queued',$command,$days);
// If not subscribed
} else {
Log::debug(sprintf('%s:- FTN [%s] is NOT subscribed to [%s], NO ACTION taken',self::LOGKEY,$this->mo->fftn->ftn,$area));
return sprintf('%-25s <-- NOT subscribed, NO ACTION taken',$command);
}
} else {
Log::debug(sprintf('%s:- FTN [%s] area UNKNOWN [%s], NO ACTION taken',self::LOGKEY,$this->mo->fftn->ftn,$area));
return sprintf('%-25s <-- AREA UNKNOWN, NO ACTION TAKEN',$command);
}
}
}

View File

@@ -1,104 +0,0 @@
<?php
namespace App\Classes\FTN\Process\Netmail\Robot;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Log;
use App\Classes\FTN\Process\Netmail\Robot;
use App\Models\{Echomail,Netmail};
use App\Notifications\Netmails\Filefix\CommandsProcessed;
/**
* Process messages to Ping
*
* @package App\Classes\FTN\Process
*/
final class Filefix extends Robot
{
private const LOGKEY = 'RPF';
public const commands = 'App\\Classes\\FTN\\Process\\Netmail\\Robot\\Filefix\\';
public static function handle(Echomail|Netmail $mo): bool
{
if ((strtolower($mo->to) !== 'filefix') || (! ($mo instanceof Netmail)))
return FALSE;
Log::info(sprintf('%s:- Processing FILEFIX [%s] message from (%s) [%s]',self::LOGKEY,$mo->to,$mo->from,$mo->fftn->ftn));
return parent::handle($mo);
}
public static function filefix(Netmail $mo): bool
{
$result = collect();
$result->push('--> BEGIN <--');
foreach ($mo->body_lines as $command) {
Log::debug(sprintf('%s:* Processing command [%s]',self::LOGKEY,$command));
// Skip empty lines
if (! $command || preg_match('/^\s+$/',$command))
continue;
$command = explode(' ',strtoupper(rtrim($command)));
Log::debug(sprintf('%s:* Processing command',self::LOGKEY),['command'=>$command]);
// If command starts with '...' or '---', its a tear/tag line, and we have reached the end
if (str_starts_with($command[0],'...') || str_starts_with($command[0],'---')) {
Log::info(sprintf('%s:= We got a tearline/tagline, end of processing',self::LOGKEY));
$result->push('--> END OF PROCESSING - TEARLINE/TAGLINE <--');
break;
// Lines starting with a space, we'll abort
} elseif (! $command[0]) {
Log::info(sprintf('%s:= Got a new line with a space, end of processing',self::LOGKEY));
$result->push('--> END OF PROCESSING - SPACE DETECTED <--');
break;
// If command doesnt start with %, its an area
} elseif (! str_starts_with($command[0],'%')) {
Log::info(sprintf('%s:= Assuming command [%s] is an AREA command',self::LOGKEY,$command[0]));
array_unshift($command,'%AREA');
}
// Some commands are reserved words
switch ($x=strtolower(substr($command[0],1))) {
case 'list':
$class = self::commands.'AreaList';
break;
default:
// Parse the message body and pluck out the commands on each line
$class = self::commands.ucfirst($x);
}
if (! class_exists($class)) {
$result->push(sprintf('%-25s <-- **COMMAND UNKNOWN**',join(' ',$command)));
Log::info(sprintf('%s:! Command UNKNOWN [%s] ',self::LOGKEY,join('|',$command)),['class'=>$class]);
continue;
}
// Drop the command from the array, the rest are arguments
array_shift($command);
// Refresh our echoareas
$mo->fftn->load('fileareas');
$o = new $class($mo,$command);
$result->push($o->process());
}
// Reply with a confirmation of what commands were processed
Notification::route('netmail',$mo->fftn)->notify(new CommandsProcessed($mo,$result));
return TRUE;
}
}

View File

@@ -1,148 +0,0 @@
<?php
namespace App\Classes\FTN\Process\Netmail\Robot\Filefix;
use Carbon\Carbon;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Classes\FTN\Process\Netmail\Robot\Areafix\Base;
use App\Jobs\FilefixRescan;
// Filearea Processing Command
class Area extends Base
{
private const LOGKEY = 'FFA';
private const command = '%AREA';
public static function help(): array
{
return [
self::command.' [-|+]<FILEAREA> [R|D=<DAYS>]',
' Use the area command to subscribe (+) or unsubscribe (-) to a FILEAREA',
' Arguments:',
' - FILEAREA (required) name of area to subscribe or unsubscribe',
' - D=DAYS (optional) number of days to resend files from this area that you',
' havent already received (useful if you are resubscribing to an area and',
' have received files in the past)',
' - R=DAYS (optional) number of days to resend files from this area (even if',
' it was sent to you previously)',
' Notes:',
' * "+" is optional, and is implied if "-" is not used',
' * "R" and "D" options only apply to subscribing',
];
}
public function process(): string
{
$command = self::command.' '.join(' ',$this->arguments);
if (! ($area=Arr::get($this->arguments,0,NULL)))
return sprintf('%-25s <-- INVALID, AN AREA IS REQUIRED',$command);
// If command starts with '-', its to unsubscribe
if (str_starts_with($area,'-')) {
$sub = FALSE;
$area = substr($area,1);
} elseif (str_starts_with($area,'+')) {
$sub = TRUE;
$area = substr($area,1);
} else {
$sub = TRUE;
$area = $area;
}
Log::debug(sprintf('%s:- Processing [%s] for [%s]',self::LOGKEY,$sub ? 'ADD' : 'REMOVE',$area));
// Drop the area from the arguments, the rest are options
array_shift($this->arguments);
// Area exists
if ($fa=$this->mo->fftn->domain->fileareas->where('name',$area)->pop()) {
// If already subscribed
if ($nea=$this->mo->fftn->fileareas->where('name',$area)->pop()) {
// requesting to subscribe "You already are since..., arguments ignored
if ($sub) {
Log::debug(sprintf('%s:- FTN [%s] ALREADY subscribed to [%s] since [%s]',self::LOGKEY,$this->mo->fftn->ftn,$area,$nea->pivot->subscribed));
return sprintf('%-25s <-- ALREADY subscribed since %s',$command,$nea->pivot->subscribed);
// requesting to unsubscribe
} else {
$this->mo->fftn->fileareas()->detach($fa->id);
// Remove sub, clear queue
$x = DB::table('file_seenby')
->where('address_id',$this->mo->fftn->id)
->join('files',['files.id'=>'file_seenby.file_id'])
->where('filearea_id',$nea->id)
->whereNotNull('export_at')
->whereNull('sent_at')
->orderBy('files.datetime')
->skip(1) // Might already being sent in this session
->delete();
Log::debug(sprintf('%s:- FTN [%s] UNSUBSCRIBED from [%s] clearing [%s]',self::LOGKEY,$this->mo->fftn->ftn,$area,$x));
return sprintf('%-25s <-- UNSUBSCRIBED, CLEARED [%d] FILES from queue',$command,$x);
}
// If not subscribed
} else {
// requesting to subscribe, subsubsribe and rescan if arguments
if ($sub) {
$this->mo->fftn->fileareas()->attach([$fa->id=>['subscribed'=>Carbon::now()]]);
// If we have arguments, they are to rescan
if (count($this->arguments) === 1) {
$m = [];
if (preg_match('/^([DR])=([0-9]+)$/',$this->arguments[0],$m)) {
switch ($m[1]) {
// Scan
case 'D':
FilefixRescan::dispatch($this->mo->fftn,$fa,$m[2])
->onQueue('mail');
return sprintf('%-25s <-- SUBSCRIBED, RESCAN [%d] DAYS queued',$command,$m[2]);
// Scan
case 'R':
FilefixRescan::dispatch($this->mo->fftn,$fa,$m[2],TRUE)
->onQueue('mail');
return sprintf('%-25s <-- SUBSCRIBED, FORCE RESCAN [%d] DAYS queued',$command,$m[2]);
}
}
return sprintf('%-25s <-- SUBSCRIBED, INVALID OPTIONS',$command);
} elseif (count($this->arguments) > 1) {
Log::debug(sprintf('%s:- FTN [%s] subscribed to [%s], extra commands [%s] ignored',self::LOGKEY,$this->mo->fftn->ftn,$area,join('|',$this->arguments)));
return sprintf('%-25s <-- SUBSCRIBED, OPTIONS IGNORED',$command);
} else {
Log::debug(sprintf('%s:- FTN [%s] subscribed to [%s]',self::LOGKEY,$this->mo->fftn->ftn,$area));
return sprintf('%-25s <-- SUBSCRIBED',$command);
}
// If not subscribed, "you arent subscribed, arguments ignored"
} else {
Log::debug(sprintf('%s:- FTN [%s] is NOT subscribed to [%s], NO ACTION taken',self::LOGKEY,$this->mo->fftn->ftn,$area));
return sprintf('%-25s <-- NOT subscribed, NO ACTION taken',$command);
}
}
} else {
Log::debug(sprintf('%s:- FILE [%s] area UNKNOWN [%s], NO ACTION taken',self::LOGKEY,$this->mo->fftn->ftn,$area));
return sprintf('%-25s <-- AREA UNKNOWN, NO ACTION TAKEN',$command);
}
}
}

View File

@@ -1,39 +0,0 @@
<?php
namespace App\Classes\FTN\Process\Netmail\Robot\Filefix;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
use App\Classes\FTN\Process\Netmail\Robot\Areafix\Base;
use App\Notifications\Netmails\Filefix\AreaList as AreaListNotification;
// LIST - List fileareas in a domain
class AreaList extends Base
{
private const LOGKEY = 'AFS';
private const command = '%LIST';
public static function help(): array
{
return [
self::command,
' List the available fileareas in this network',
];
}
public function process(): string
{
Log::debug(sprintf('%s:- Filefix [%s] for [%s] for [%s]',self::LOGKEY,self::command,$this->mo->fftn->ftn,join('|',$this->arguments)));
if (count($this->arguments) > 1)
return sprintf('%-25s <-- INVALID COMMAND',self::command);
else {
Notification::route('netmail',$this->mo->fftn)
->notify(new AreaListNotification($this->mo));
return sprintf('%-25s <-- COMMAND PROCESSED',self::command);
}
}
}

View File

@@ -1,71 +0,0 @@
<?php
namespace App\Classes\FTN\Process\Netmail\Robot\Filefix;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
use App\Classes\FTN\Process\Netmail\Robot\Areafix\Base;
use App\Notifications\Netmails\Filefix\Filelist as FilelistNotification;
// FILELIST - List files in an area
class Filelist extends Base
{
private const LOGKEY = 'AFR';
private const command = '%FILELIST';
public static function help(): array
{
return [
self::command.' [-|+]<FILEAREA> [<DAYS>]',
' Use the filelist command to list files from an filearea.',
' This is will resend files again, even if you have received them in the',
' past.',
' Arguments:',
' - FILEAREA (required) name of area',
' - DAYS (optional) number of days to resend mail from this area that you',
' If DAYS is omitted, the default is 30. The maximum is 365.',
];
}
public function process(): string
{
Log::debug(sprintf('%s:- Filefix [%s] for [%s] for [%s]',self::LOGKEY,self::command,$this->mo->fftn->ftn,join('|',$this->arguments)));
$command = self::command.' '.join(' ',$this->arguments);
if (! is_numeric($days=Arr::get($this->arguments,1,30)))
return sprintf('%-25s <-- INVALID, DAYS [%s] NOT NUMERIC',$command,$this->arguments[1]);
if ($days > 365)
$days = 365;
if (! ($area=Arr::get($this->arguments,0)))
return sprintf('%-25s <-- INVALID, AN AREA IS REQUIRED',$command);
// Area exists
if ($fa=$this->mo->fftn->domain->fileareas->where('name',$area)->pop()) {
// If already subscribed
if ($this->mo->fftn->fileareas->pluck('name')->contains($area)) {
Notification::route('netmail',$this->mo->fftn)
->notify(new FileListNotification($this->mo,$fa,$days));
Log::debug(sprintf('%s:- FTN [%s] FILELIST [%s] DAYS [%d]',self::LOGKEY,$this->mo->fftn->ftn,$area,$days));
return sprintf('%-25s <-- FILELIST [%d] DAYS',$command,$days);
// If not subscribed
} else {
Log::debug(sprintf('%s:- FTN [%s] is NOT subscribed to [%s], NO ACTION taken',self::LOGKEY,$this->mo->fftn->ftn,$area));
return sprintf('%-25s <-- NOT subscribed, NO ACTION taken',$command);
}
} else {
Log::debug(sprintf('%s:- FILE [%s] area UNKNOWN [%s], NO ACTION taken',self::LOGKEY,$this->mo->fftn->ftn,$area));
return sprintf('%-25s <-- AREA UNKNOWN, NO ACTION TAKEN',$command);
}
}
}

View File

@@ -1,50 +0,0 @@
<?php
namespace App\Classes\FTN\Process\Netmail\Robot\Filefix;
use App\Classes\FTN\Process\Netmail\Robot\Areafix\Base;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
use App\Classes\FTN\Process\Netmail\Robot\Filefix;
use App\Notifications\Netmails\Filefix\Help as HelpNotification;
// A Help Index Command
class Help extends Base
{
private const LOGKEY = 'FFH';
private const filefix_classes = 'app/Classes/FTN/Process/Netmail/Robot/Filefix';
private const command = '%HELP';
public static function help(): array
{
return [
self::command,
' This message!',
];
}
public function process(): string
{
Log::debug(sprintf('%s:- Processing [%s] for [%s]',self::LOGKEY,self::command,$this->mo->fftn->ftn));
$result = collect();
foreach (preg_grep('/^([^.])/',scandir(self::filefix_classes)) as $file) {
if (($file === 'Base.php') || (! str_ends_with(strtolower($file),'.php')))
continue;
$class = Filefix::commands.preg_replace('/\.php$/','',$file);
if ($result->count())
$result->push('');
$result = $result
->merge($class::help());
}
Notification::route('netmail',$this->mo->fftn)->notify(new HelpNotification($this->mo,$result));
return sprintf('%-25s <-- COMMAND PROCESSED',self::command);
}
}

View File

@@ -1,70 +0,0 @@
<?php
namespace App\Classes\FTN\Process\Netmail\Robot\Filefix;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use App\Classes\FTN\Process\Netmail\Robot\Areafix\Base;
use App\Jobs\FilefixRescan;
// RESCAN - Resend echomail
class Rescan extends Base
{
private const LOGKEY = 'AFR';
private const command = '%RESCAN';
public static function help(): array
{
return [
self::command.' <FILEAREA> [<DAYS>]',
' Use the rescan command to resend files from a filearea.',
' This is will resend files again, even if you have received them in the',
' past.',
' Arguments:',
' - FILEAREA (required) name of area to subscribe or unsubscribe',
' - DAYS (optional) number of days to resend mail from this area that you',
' If DAYS is omitted, the default is 30. The maximum is 365.',
];
}
public function process(): string
{
Log::debug(sprintf('%s:- Filefix [%s] for [%s] for [%s]',self::LOGKEY,self::command,$this->mo->fftn->ftn,join('|',$this->arguments)));
$command = self::command.' '.join(' ',$this->arguments);
if (! ($area=Arr::get($this->arguments,0)))
return sprintf('%-25s <-- INVALID, AN AREA IS REQUIRED',$command);
if (! is_numeric($days=Arr::get($this->arguments,1,30)))
return sprintf('%-25s <-- INVALID, DAYS [%s] NOT NUMERIC',$command,$this->arguments[1]);
if ($days > 365)
$days = 365;
// Area exists
if ($fa=$this->mo->fftn->domain->fileareas->where('name',$area)->pop()) {
// If already subscribed
if ($this->mo->fftn->fileareas->pluck('name')->contains($area)) {
FilefixRescan::dispatch($this->mo->fftn,$fa,$days,TRUE)
->onQueue('mail');
Log::debug(sprintf('%s:- FTN [%s] RESCAN [%s] DAYS [%d]',self::LOGKEY,$this->mo->fftn->ftn,$area,$days));
return sprintf('%-25s <-- RESCAN [%d] DAYS queued',$command,$days);
// If not subscribed
} else {
Log::debug(sprintf('%s:- FTN [%s] is NOT subscribed to [%s], NO ACTION taken',self::LOGKEY,$this->mo->fftn->ftn,$area));
return sprintf('%-25s <-- NOT subscribed, NO ACTION taken',$command);
}
} else {
Log::debug(sprintf('%s:- FILE [%s] area UNKNOWN [%s], NO ACTION taken',self::LOGKEY,$this->mo->fftn->ftn,$area));
return sprintf('%-25s <-- AREA UNKNOWN, NO ACTION TAKEN',$command);
}
}
}

View File

@@ -1,79 +0,0 @@
<?php
namespace App\Classes\FTN\Process\Netmail\Robot\Filefix;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use App\Classes\FTN\Process\Netmail\Robot\Areafix\Base;
// Resend a file
class Resend extends Base
{
private const LOGKEY = 'FFA';
private const command = '%RESEND';
public static function help(): array
{
return [
self::command.' <FILEAREA> <FILENAME>',
' Resend a file from a file area',
' Arguments:',
' - FILEAREA (required) name of area to subscribe or unsubscribe',
' - FILENAME (required) number of file to resend',
' Notes:',
' * You can obtain a list of files in an area with %FILELIST <FILEAREA>',
];
}
public function process(): string
{
$command = self::command.' '.join(' ',$this->arguments);
if (count($this->arguments) < 2)
return sprintf('%-25s <-- INVALID, NOT ENOUGH ARGUMENTS',$command);
elseif (count($this->arguments) > 2)
return sprintf('%-25s <-- INVALID, TOO MANY ARGUMENTS',$command);
Log::debug(sprintf('%s:- Resending [%s] from [%s] to [%s]',self::LOGKEY,$this->arguments[1],$this->arguments[0],$this->mo->fftn->ftn));
// Area exists
if ($fa=$this->mo->fftn->domain->fileareas->where('name',$this->arguments[0])->pop()) {
// If already subscribed
if ($nea=$this->mo->fftn->fileareas->where('name',$fa->name)->pop()) {
// Check the file is in the area
if ($fo=$nea->files()->where('name','ilike',$this->arguments[1])->single()) {
// File hasnt been exported before
if (! $fo->seenby->where('id',$this->mo->fftn_id)->count()) {
Log::info(sprintf('Exported [%d] FILE (%s) dated (%s) to [%s]',$fo->id,$fo->name,$fo->datetime->format('Y-m-d H:i:s'),$this->mo->fftn->ftn3d));
$fo->seenby()->attach($this->mo->fftn_id,['export_at'=>Carbon::now()]);
} else {
Log::debug(sprintf('Re-exported [%d] FILE (%s) dated (%s) to [%s]',$fo->id,$fo->name,$fo->datetime,$this->mo->fftn->ftn3d));
$fo->seenby()->updateExistingPivot($this->mo->fftn_id,['export_at'=>Carbon::now(),'sent_at'=>NULL]);
}
return sprintf('%-25s <-- FILE QUEUED TO RESEND',$command);
// No file in area
} else {
Log::debug(sprintf('%s:- FTN [%s] doesnt have a file [%s], NO ACTION taken',self::LOGKEY,$this->mo->fftn->ftn,$this->arguments[1]));
return sprintf('%-25s <-- FILE NOT FOUND, NO ACTION taken',$command);
}
// If not subscribed
} else {
Log::debug(sprintf('%s:- FTN [%s] is NOT subscribed to [%s], NO ACTION taken',self::LOGKEY,$this->mo->fftn->ftn,$this->arguments[0]));
return sprintf('%-25s <-- NOT subscribed, NO ACTION taken',$command);
}
} else {
Log::debug(sprintf('%s:- FILE [%s] area UNKNOWN [%s], NO ACTION taken',self::LOGKEY,$this->mo->fftn->ftn,$this->arguments[0]));
return sprintf('%-25s <-- AREA UNKNOWN, NO ACTION TAKEN',$command);
}
}
}

View File

@@ -1,70 +0,0 @@
<?php
namespace App\Classes\FTN\Process\Netmail\Robot\Filefix;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use App\Classes\FTN\Process\Netmail\Robot\Areafix\Base;
use App\Jobs\FilefixRescan;
// SCAN - Send unsent echomail
class Scan extends Base
{
private const LOGKEY = 'AFS';
private const command = '%SCAN';
public static function help(): array
{
return [
self::command.' <FILEAREA> [<DAYS>]',
' Use the scan command to resend files that you havent received yet from an',
' filearea. This is useful if you are rejoining an filearea, and only want',
' to get files that you dont already have.',
' Arguments:',
' - FILEAREA (required) name of area to subscribe or unsubscribe',
' - DAYS (optional) number of days to resend mail from this area that you',
' If DAYS is omitted, the default is 30. The maximum is 365.',
];
}
public function process(): string
{
Log::debug(sprintf('%s:- Filefix [%s] for [%s] for [%s]',self::LOGKEY,self::command,$this->mo->fftn->ftn,join('|',$this->arguments)));
$command = self::command.' '.join(' ',$this->arguments);
if (! ($area=Arr::get($this->arguments,0)))
return sprintf('%-25s <-- INVALID, AN AREA IS REQUIRED',$command);
if (! is_numeric($days=Arr::get($this->arguments,1,30)))
return sprintf('%-25s <-- INVALID, DAYS [%s] NOT NUMERIC',$command,$this->arguments[1]);
if ($days > 365)
$days = 365;
// Area exists
if ($fa=$this->mo->fftn->domain->fileareas->where('name',$area)->pop()) {
// If already subscribed
if ($this->mo->fftn->fileareas->pluck('name')->contains($area)) {
FilefixRescan::dispatch($this->mo->fftn,$fa,$days)
->onQueue('mail');
Log::debug(sprintf('%s:- FTN [%s] SCAN [%s] DAYS [%d]',self::LOGKEY,$this->mo->fftn->ftn,$area,$days));
return sprintf('%-25s <-- SCAN [%d] DAYS queued',$command,$days);
// If not subscribed
} else {
Log::debug(sprintf('%s:- FTN [%s] is NOT subscribed to [%s], NO ACTION taken',self::LOGKEY,$this->mo->fftn->ftn,$area));
return sprintf('%-25s <-- NOT subscribed, NO ACTION taken',$command);
}
} else {
Log::debug(sprintf('%s:- FILE [%s] area UNKNOWN [%s], NO ACTION taken',self::LOGKEY,$this->mo->fftn->ftn,$area));
return sprintf('%-25s <-- AREA UNKNOWN, NO ACTION TAKEN',$command);
}
}
}

View File

@@ -106,7 +106,7 @@ class Tic extends FTNBase
$result->put('REPLACES',$this->file->replaces);
$result->put('AREA',$this->file->filearea->name);
$result->put('AREADESC',$this->file->filearea->description);
if ($x=$this->to->pass_tic)
if ($x=strtoupper($this->to->session('ticpass')))
$result->put('PW',$x);
$result->put('CRC',sprintf("%X",$this->file->crc));
@@ -135,7 +135,7 @@ class Tic extends FTNBase
*/
public function isNodelist(): bool
{
Log::info(sprintf('%s:D fo_nodelist_file_area [%d], fo_filearea_domain_filearea_id [%d], regex [%s] name [%s]',
Log::critical(sprintf('%s:D fo_nodelist_file_area [%d], fo_filearea_domain_filearea_id [%d], regex [%s] name [%s]',
self::LOGKEY,
$this->file->nodelist_filearea_id,
$this->file->filearea->domain->filearea_id,
@@ -219,8 +219,6 @@ class Tic extends FTNBase
case 'from':
if (($ao=Address::findFTN($m[2])) && ((! $aid) || ($ao->zone->domain_id === Address::findOrFail(hexdec($aid))->zone->domain_id)))
$this->file->fftn_id = $ao->id;
elseif ($aid && ($x=Address::findOrFail(hexdec($aid))) && (($y=$x->system->akas->search(fn($item)=>str_starts_with($item->ftn,$m[2]))) !== FALSE))
$this->file->fftn_id = $x->system->akas->get($y)->id;
else
throw new ModelNotFoundException(sprintf('FTN Address [%s] not found or sender mismatch',$m[2]));
@@ -349,11 +347,11 @@ class Tic extends FTNBase
// @todo Add notification back to the system if no replaces line and the file already exists
// Validate Size
if ((! is_null($this->file->size)) && ($this->file->size !== ($y=$fs->size($file))))
if ($this->file->size !== ($y=$fs->size($file)))
throw new SizeMismatchException(sprintf('TIC file size [%d] doesnt match file [%s] (%d)',$this->file->size,$fs->path($rel_path_name),$y));
// Validate Password
if (strtoupper($pw) !== ($y=$this->file->fftn->pass_tic))
if (strtoupper($pw) !== ($y=strtoupper($this->file->fftn->session('ticpass'))))
throw new InvalidPasswordException(sprintf('TIC file PASSWORD [%s] doesnt match system [%s] (%s)',$pw,$this->file->fftn->ftn,$y));
// Validate Sender is linked
@@ -369,10 +367,6 @@ class Tic extends FTNBase
if (! $this->file->datetime)
$this->file->datetime = Carbon::createFromTimestamp($fs->lastModified($file));
// If the file size was omitted, we'll use the file's size
if (is_null($this->file->size))
$this->file->size = $fs->size($file);
$this->file->src_file = $file;
$this->file->recv_tic = $filename;

View File

@@ -22,11 +22,7 @@ class File extends FileBase implements \Iterator
{
parent::__construct($path,$checkPath);
if ($this->getExtension() === 'pkt')
$this->canHandle = TRUE;
else
switch ($x=$this->guessExtension()) {
switch($x=$this->guessExtension()) {
case 'zip':
$this->canHandle = TRUE;
$this->isArchive = TRUE;
@@ -87,11 +83,6 @@ class File extends FileBase implements \Iterator
/* METHODS */
public function isArchive(): bool
{
return $this->isArchive;
}
/**
* Determine if the file is a mail packet
*

View File

@@ -11,7 +11,7 @@ use Illuminate\Support\Collection;
* When sending, we can queue up a list of items, and mark one active (the one being sent) at a time.
*
* + Netmail/Echomail/TIC
* + name is the hex ID of the youngest item in the mail bundle
* + name is dynamically calculated, based on timew() of the youngest item in the mail bundle
* + size is dynamically calculated based on the size of the bundle
* + mtime is dynamically calculated, based on the age of the youngest item
* + sendas (nameas) is name + [.pkt|.tic]

View File

@@ -41,6 +41,7 @@ final class File extends Send
return $this->f->datetime->timestamp;
case 'name':
case 'size':
return $this->f->{$key};
case 'type':
@@ -83,12 +84,12 @@ final class File extends Send
*/
public function open(string $compress=''): bool
{
$this->size = $this->f->size;
// If sending file is a File::class, then our file is s3
$this->fd = ($this->nameas && $this->f instanceof FileModel)
? Storage::readStream($this->f->rel_name)
: fopen($this->full_name,'rb');
if ($this->nameas && $this->f instanceof FileModel) {
$this->fd = Storage::readStream($this->f->rel_name);
} else {
$this->fd = fopen($this->full_name,'rb');
if (! $this->fd) {
Log::error(sprintf('%s:! Unable to open file [%s] for reading',self::LOGKEY,$this->full_name));
@@ -97,6 +98,7 @@ final class File extends Send
}
Log::info(sprintf('%s:= File [%s] opened with size [%d]',self::LOGKEY,$this->full_name,$this->size));
}
return TRUE;
}
@@ -108,6 +110,6 @@ final class File extends Send
public function seek(int $pos): bool
{
return (fseek($this->fd,$pos,SEEK_SET) === 0);
return (fseek($this->f,$pos,SEEK_SET) === 0);
}
}

View File

@@ -4,7 +4,6 @@ namespace App\Classes\File;
use Carbon\Carbon;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@@ -37,13 +36,16 @@ final class Mail extends Send
return $this->f->messages->pluck('id');
case 'name':
return sprintf('%08x',$this->youngest_id());
return sprintf('%08x',timew($this->youngest()));
case 'nameas':
return sprintf('%s.pkt',$this->name);
case 'mtime':
return $this->youngest_date()->timestamp;
return $this->youngest()->timestamp;
case 'size':
return strlen($this->f);
case 'type':
return ($this->ftype&0xff00)>>8;
@@ -58,7 +60,7 @@ final class Mail extends Send
if ($successful) {
$this->complete = TRUE;
Log::info(sprintf('%s:- Successful close for [%d] - updating [%d] records.',self::LOGKEY,$this->type,$this->dbids->count()),['dbids'=>$this->dbids,'authd'=>$node->aka_remote_authed->pluck('id')]);
Log::debug(sprintf('%s:- Successful close for [%d] - updating [%d] records.',self::LOGKEY,$this->type,$this->dbids->count()),['dbids'=>$this->dbids,'authd'=>$node->aka_remote_authed->pluck('id')]);
// Update netmail table
if (($this->type === Send::T_NETMAIL)
@@ -85,7 +87,6 @@ final class Mail extends Send
]);
$this->content = NULL;
$this->f = NULL;
}
}
@@ -97,7 +98,6 @@ final class Mail extends Send
public function open(string $compress=''): bool
{
$this->content = (string)$this->f;
$this->size = strlen($this->content);
return TRUE;
}
@@ -115,18 +115,8 @@ final class Mail extends Send
return TRUE;
}
private function youngest(): array
private function youngest(): Carbon
{
return $this->f->messages->sortBy(fn($item)=>Arr::get($item,'datetime'))->first();
}
private function youngest_id(): int
{
return Arr::get($this->youngest(),'id',0);
}
private function youngest_date(): Carbon
{
return Arr::get($this->youngest(),'datetime',Carbon::now());
return $this->f->messages->pluck('date')->sort()->last();
}
}

View File

@@ -128,9 +128,9 @@ class Receive extends Base
// If packet is greater than a size, lets queue it
if ($this->queue || ($this->receiving->size > config('fido.queue_size',0))) {
Log::info(sprintf('%s:- Packet [%s] will be sent to the queue for processing because its [%d] size, or queue forced',self::LOGKEY,$this->receiving->full_name,$this->receiving->size));
PacketProcess::dispatch($this->receiving->rel_name,$this->ao->system,FALSE,$rcvd_time);
PacketProcess::dispatch($this->receiving->rel_name,$this->ao->zone->domain,FALSE,$rcvd_time);
} else
PacketProcess::dispatchSync($this->receiving->rel_name,$this->ao->system,TRUE,$rcvd_time);
PacketProcess::dispatchSync($this->receiving->rel_name,$this->ao->zone->domain,TRUE,$rcvd_time);
} catch (\Exception $e) {
Log::error(sprintf('%s:! Got error dispatching packet [%s] (%d:%s-%s).',self::LOGKEY,$this->receiving->rel_name,$e->getLine(),$e->getFile(),$e->getMessage()));

View File

@@ -34,7 +34,6 @@ class Send extends Base
public const T_ECHOMAIL = (1<<2);
private string $comp_data;
protected int $size = 0;
public function __construct()
{
@@ -125,9 +124,6 @@ class Send extends Base
if ($successful) {
$end = time()-$this->start;
Log::info(sprintf('%s:- Closing [%s], sent in [%d] with [%s] items',self::LOGKEY,$this->sending->nameas,$end,$this->sending->dbids->count()));
} else {
Log::alert(sprintf('%s:- Closing [%s], file NOT SENT successfully',self::LOGKEY,$this->sending->nameas));
}
$this->sending->close($successful,$node);
@@ -203,8 +199,8 @@ class Send extends Base
}
// Files
if (($x=$ao->getFiles())->count()) {
Log::info(sprintf('%s:- [%d] Files(s) added for sending to [%s]',self::LOGKEY,$x->count(),$ao->ftn));
if (($x=$ao->filesWaiting())->count()) {
Log::debug(sprintf('%s:- [%d] Files(s) added for sending to [%s]',self::LOGKEY,$x->count(),$ao->ftn));
// Add Files
foreach ($x as $fo) {
@@ -227,12 +223,12 @@ class Send extends Base
*/
public function open(string $compress=''): bool
{
Log::debug(sprintf('%s:+ File Open to send',self::LOGKEY));
Log::debug(sprintf('%s:+ Opening file to send',self::LOGKEY));
if ((($this->index=$this->list->search(function($item) { return $item->complete === FALSE; })) !== FALSE)
&& $this->sending->open())
{
Log::info(sprintf('%s:- Content Send item [#%d] (%s) with size [%d]',self::LOGKEY,$this->index,$this->sending->nameas,$this->sending->size));
Log::info(sprintf('%s:- Sending item [%d] (%s)',self::LOGKEY,$this->index,$this->sending->nameas));
$this->pos = 0;
$this->start = time();
@@ -245,10 +241,7 @@ class Send extends Base
return TRUE;
} else {
Log::error(sprintf('%s:- No files to open',self::LOGKEY));
$this->index = NULL;
return FALSE;
throw new Exception('No files to open');
}
}
@@ -309,7 +302,7 @@ class Send extends Base
$this->pos += strlen($data);
Log::debug(sprintf('%s:- Content Read [%d] bytes, pos now [%d]',self::LOGKEY,strlen($data),$this->pos));
Log::debug(sprintf('%s:- Read [%d] bytes, file pos now [%d]',self::LOGKEY,strlen($data),$this->pos));
return $data;
}
@@ -329,7 +322,7 @@ class Send extends Base
if ($this->sending->seek($pos)) {
$this->pos = $pos;
Log::debug(sprintf('%s:= Content Seek to [%d]',self::LOGKEY,$this->pos));
Log::debug(sprintf('%s:= Seeked to [%d]',self::LOGKEY,$this->pos));
return TRUE;

View File

@@ -3,7 +3,6 @@
namespace App\Classes\File\Send;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use App\Classes\File\Send;
use App\Classes\Node;
@@ -48,7 +47,7 @@ final class Dynamic extends Send
return $this->sent->timestamp;
case 'size':
return $this->{$key};
return strlen($this->buffer);
default:
return NULL;
@@ -57,8 +56,6 @@ final class Dynamic extends Send
public function close(bool $successful,Node $node): void
{
Log::debug(sprintf('%s:- Close [%s] - %s',self::LOGKEY,$this->nameas,$successful ? 'SUCCESSFUL' : 'FAILED'));
if ($successful) {
$this->complete = TRUE;
@@ -77,35 +74,24 @@ final class Dynamic extends Send
$this->do->next_at = $next_at
->addDay();
while ($this->do->next_at->isPast())
$this->do->next_at = $this->do->next_at->addDay();
break;
case 'WEEKLY':
$this->do->next_at = $next_at
->addWeek();
while ($this->do->next_at->isPast())
$this->do->next_at = $this->do->next_at->addWeek();
break;
case 'MONTHLY':
$this->do->next_at = $next_at
->addMonth();
while ($this->do->next_at->isPast())
$this->do->next_at = $this->do->next_at->addMonth();
break;
default:
throw new \Exception(sprintf('%s:! Unknown frequency [%s] for [%d]',self::LOGKEY,$this->do->frequency,$this->do->id));
}
Log::debug(sprintf('%s: - Frequency [%s], UPDATE next_at [%s]',self::LOGKEY,$this->do->frequency,$next_at->format('Y-m-d H:i:s')));
$this->do->save();
}
}
@@ -118,7 +104,6 @@ final class Dynamic extends Send
public function open(string $compress=''): bool
{
$this->buffer = (string)$this->item;
$this->size = strlen($this->buffer);
return TRUE;
}

View File

@@ -43,6 +43,9 @@ final class Tic extends Send
case 'mtime':
return $this->f->datetime->timestamp;
case 'size':
return strlen($this->tic);
case 'type':
return ($this->ftype&0xff00)>>8;
@@ -64,8 +67,6 @@ final class Tic extends Send
public function open(string $compress=''): bool
{
$this->size = strlen($this->tic);
return TRUE;
}

View File

@@ -1,201 +0,0 @@
<?php
namespace App\Classes\Fonts;
use App\Classes\Font;
final class Ansitex extends Font
{
protected const FONT = [
'a' => [
[0xda,0xc4,0xdc],
[0xb3,0xdf,0xdb],
[0x20,0x20,0x20],
],
'b' => [
[0xb3,0xc4,0xdc],
[0xb3,0x5f,0xdb],
[0x20,0x20,0x20],
],
'c' => [
[0xda,0xc4,0xdc],
[0xb3,0x5f,0xdc],
[0x20,0x20,0x20],
],
'd' => [
[0xda,0xc4,0xdb],
[0xb3,0x5f,0xdb],
[0x20,0x20,0x20],
],
'e' => [
[0xda,0xc4,0xdc],
[0xc3,0x5f,0xdc],
[0x20,0x20,0x20],
],
'f' => [
[0xda,0xc4,0xdc],
[0xc3,0x20,0x20],
[0x20,0x20,0x20],
],
'g' => [
[0xda,0xc4,0xdc],
[0xb3,0x5f,0xdb],
[0x20,0xc4,0xdf],
],
'h' => [
[0xde,0xc4,0xdc],
[0xde,0x20,0xdb],
[0x20,0x20,0x20],
],
'i' => [
[0x20,0xdf],
[0xde,0xdb],
[0x20,0x20],
],
'j' => [
[0x20,0xdf],
[0xde,0xdb],
[0xc4,0xdf],
],
'k' => [
[0xb3,0x20,0xdc],
[0xb3,0x60,0xdc],
[0x20,0x20,0x20],
],
'l' => [
[0xb3,0x20],
[0xb3,0xdc],
[0x20,0x20],
],
'm' => [
[0xda,0xda,0xdc],
[0xb3,0xb3,0xdb],
[0x20,0x20,0x20],
],
'n' => [
[0xda,0xc4,0xdc],
[0xb3,0x20,0xdb],
[0x20,0x20,0x20],
],
'o' => [
[0xda,0xc4,0xdc],
[0xb3,0x5f,0xdb],
[0x20,0x20,0x20],
],
'p' => [
[0xda,0xc4,0xdc],
[0xb3,0x5f,0xdb],
[0xc0,0x20,0x20],
],
'q' => [
[0xda,0xc4,0xdc],
[0xb3,0x5f,0xdb],
[0x20,0x20,0xdf],
],
'r' => [
[0xda,0xc4,0xdc],
[0xb3,0xc1,0x5c],
[0x20,0x20,0x20],
],
's' => [
[0xda,0xc4,0xdc],
[0x2e,0x5c,0xdc],
[0x20,0x20,0x20],
],
't' => [
[0xda,0xc2,0xdc],
[0x20,0xb3,0x20],
[0x20,0x20,0x20],
],
'u' => [
[0xda,0x20,0xdc],
[0xb3,0x5f,0xdb],
[0x20,0x20,0x20],
],
'v' => [
[0xda,0x20,0xdc],
[0x60,0x5c,0xdb],
[0x20,0x20,0x20],
],
'w' => [
[0xda,0xda,0xdb],
[0x60,0x5c,0xdb],
[0x20,0x20,0x20],
],
'x' => [
[0xda,0x20,0xdc],
[0xda,0xdf,0xdc],
[0x20,0x20,0x20],
],
'y' => [
[0xda,0x20,0xdc],
[0xb3,0x5f,0xdb],
[0x20,0xc4,0xdf],
],
'z' => [
[0xda,0xc4,0xdc],
[0x2e,0x2f,0xdc],
[0x20,0x20,0x20],
],
'1' => [
[0xc4,0xdc],
[0x20,0xdb],
[0x20,0x20],
],
'2' => [
[0xfe,0xc4,0xdc],
[0xda,0x2f,0xdc],
[0x20,0x20,0x20],
],
'3' => [
[0xda,0xc4,0xdc],
[0xda,0xf0,0xdb],
[0x20,0x20,0x20],
],
'4' => [
[0xde,0x20,0xdc],
[0xc0,0xc4,0xdb],
[0x20,0x20,0x20],
],
'5' => [
[0xda,0xc4,0xdc],
[0x2e,0x2d,0xdc],
[0x20,0x20,0x20],
],
'6' => [
[0xde,0xc4,0xbf],
[0xb3,0x5f,0xdb],
[0x20,0x20,0x20],
],
'7' => [
[0xfe,0x2d,0xbf],
[0x20,0xde,0x20],
[0x20,0x20,0x20],
],
'8' => [
[0xda,0xc4,0xdc],
[0xc3,0xf0,0xdb],
[0x20,0x20,0x20],
],
'9' => [
[0xda,0xc4,0xdc],
[0xd4,0xc4,0xdb],
[0x20,0x20,0x20],
],
'0' => [
[0xda,0xc4,0xdc],
[0xb3,0x5f,0xdb],
[0x20,0x20,0x20],
],
'#' => [
[0xdc,0xba,0xdc],
[0xfe,0xba,0xfe],
[0x20,0x20,0x20],
],
'!' => [
[0xdb],
[0xfe],
[0x20],
],
];
}

View File

@@ -38,7 +38,6 @@ class Node
private Collection $ftns_authed; // The FTNs we have validated
private Collection $ftns_other; // Other FTN addresses presented
private bool $authed; // Have we authenticated the remote.
private Address $originate; // When we originate a call, this is who we are after
private int $options; // This nodes capabilities/options
@@ -85,15 +84,11 @@ class Node
// The nodes password
case 'password':
// If we are originating a session, we'll use that password.
if (isset($this->originate))
return $this->originate->pass_session;
// If we have already authed, we'll use that password.
if ($this->ftns_authed->count())
return $this->ftns_authed->first()->pass_session;
return $this->ftns_authed->first()->session('sespass');
else
return ($this->ftns->count() && ($x=$this->ftns->first()->pass_session)) ? $x : '-';
return ($this->ftns->count() && ($x=$this->ftns->first()->session('sespass'))) ? $x : '-';
// Return how long our session has been connected
case 'session_time':
@@ -199,9 +194,7 @@ class Node
throw new Exception('Already authed');
foreach ($this->ftns as $o) {
Log::debug(sprintf('%s:- Attempting to authenticate [%s] with [%s]',self::LOGKEY,$o->ftn,$o->pass_session));
if (! $sespass=$o->pass_session)
if (! $sespass=$o->session('sespass'))
continue;
// If we have challenge, then we are doing MD5
@@ -276,8 +269,7 @@ class Node
*/
public function originate(Address $o): void
{
$this->originate = $o;
$this->ftns_authed = $o->system->match($o->zone,Address::NODE_ALL);
$this->ftns_authed->push($o);
}
/**
@@ -291,11 +283,19 @@ class Node
if ($this->authed)
return TRUE;
Log::debug(sprintf('%s:- Making sure we called [%s] from [%s]',self::LOGKEY,$this->originate->ftn,$this->ftns->pluck('ftn')->join(',')));
if ($this->ftns_authed->count() !== 1 || ! $this->ftns->count())
return FALSE;
$this->authed = $this->ftns->pluck('ftn')->contains($this->originate->ftn);
$ftn = $this->ftns_authed->first()->ftn;
return $this->authed;
return $this->ftns->search(function($item) use ($ftn) {
if ($item->ftn === $ftn) {
$item->system->last_session = Carbon::now();
$item->system->save();
$this->authed = TRUE;
return TRUE;
}
}) !== FALSE;
}
public function optionClear(int $key): void

View File

@@ -332,13 +332,13 @@ class Page
$subtext = substr($this->text,$current_pos,$space_pos);
}
// If the rest of the string will fit on the current line
} elseif ($text_length-$current_pos < static::MSG_WIDTH-$this->x-$buffer) {
// If the reset of the string will fit on the current line
} elseif ($text_length-$current_pos < static::MSG_WIDTH-$buffer) {
$subtext = substr($this->text,$current_pos);
// Get the next lines worth of chars, breaking on a space
} else {
$subtext = $this->text_substr(substr($this->text,$current_pos),static::MSG_WIDTH-$this->x-$buffer);
$subtext = $this->text_substr(substr($this->text,$current_pos),static::MSG_WIDTH-$buffer);
// Include the text up to the last space
if (substr($this->text,$current_pos+strlen($subtext),1) !== ' ')

View File

@@ -6,9 +6,9 @@ use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use App\Classes\File\{Receive,Send};
use App\Classes\Protocol\{Binkp,DNS,EMSI,Zmodem};
use App\Classes\Sock\Exception\SocketException;
use App\Classes\Protocol\EMSI;
use App\Classes\Sock\SocketClient;
use App\Classes\Sock\SocketException;
use App\Models\{Address,Mailer,Setup,System,SystemLog};
// @todo after receiving a mail packet/file, dont acknowledge it until we can validate that we can read it properly.
@@ -107,7 +107,7 @@ abstract class Protocol
public const TCP_SPEED = 115200;
protected SocketClient $client; /* Our socket details */
protected Setup $setup; /* Our setup */
protected ?Setup $setup; /* Our setup */
protected Node $node; /* The node we are communicating with */
/** The list of files we are sending */
protected Send $send;
@@ -122,12 +122,7 @@ abstract class Protocol
protected array $capability; // @todo make private
/** @var bool Are we originating a connection */
protected bool $originate;
/** @var bool Is the application down for maintenance */
protected bool $down = FALSE;
/** @var int Our mailer ID for logging purposes */
private int $mailer_id;
/** Our comms details */
private array $comms;
@@ -137,31 +132,12 @@ abstract class Protocol
abstract protected function protocol_session(bool $force_queue=FALSE): int;
/**
* @param Setup $setup
* @throws \Exception
*/
public function __construct(Setup $setup)
public function __construct(Setup $o=NULL)
{
$this->setup = $setup;
if ($o && ! $o->system->akas->count())
throw new \Exception('We dont have any ACTIVE FTN addresses assigned');
// Some initialisation details
switch (get_class($this)) {
case Binkp::class:
$this->mailer_id = Mailer::where('name','BINKP')->sole()->id;
break;
case DNS::class:
case Zmodem::class:
break;
case EMSI::class:
$this->mailer_id = Mailer::where('name','EMSI')->sole()->id;
break;
default:
throw new \Exception('not handled'.get_class($this));
}
$this->setup = $o;
}
/**
@@ -270,29 +246,20 @@ abstract class Protocol
* Incoming Protocol session
*
* @param SocketClient $client
* @return int
* @return int|null
* @throws SocketException
*/
public function onConnect(SocketClient $client): int
public function onConnect(SocketClient $client): ?int
{
$pid = pcntl_fork();
if ($pid === -1)
throw new SocketException(SocketException::CANT_ACCEPT,'Could not fork process');
// If our parent returns a PID, we've forked
if ($pid)
Log::info(sprintf('%s:+ New connection from [%s], thread [%d] created',self::LOGKEY,$client->address_remote,$pid));
// This is the new thread
else {
Log::withContext(['pid'=>getmypid()]);
Log::debug(sprintf('%s:* Client session starting',self::LOGKEY));
$this->session($client,(new Address));
}
Log::info(sprintf('%s:+ New connection, thread [%d] created',self::LOGKEY,$pid));
// Parent return ready for next connection
return $pid;
}
@@ -335,7 +302,6 @@ abstract class Protocol
* Our addresses to send to the remote
*
* @return Collection
* @throws \Exception
*/
protected function our_addresses(): Collection
{
@@ -350,7 +316,7 @@ abstract class Protocol
Log::debug(sprintf('%s:- Presenting limited AKAs [%s]',self::LOGKEY,$addresses->pluck('ftn')->join(',')));
} else {
$addresses = our_address();
$addresses = $this->setup->system->akas;
Log::debug(sprintf('%s:- Presenting ALL our AKAs [%s]',self::LOGKEY,$addresses->pluck('ftn')->join(',')));
}
@@ -359,14 +325,15 @@ abstract class Protocol
}
/**
* Setup a session with a remote client
* Initialise our Session
*
* @param SocketClient $client Socket details of session
* @param Address $o If we have an address, we originated a session to this Address
* @param Mailer $mo
* @param SocketClient $client
* @param Address|null $o
* @return int
* @throws \Exception
*/
public function session(SocketClient $client,Address $o): int
public function session(Mailer $mo,SocketClient $client,Address $o=NULL): int
{
if ($o->exists)
Log::withContext(['ftn'=>$o->ftn]);
@@ -397,17 +364,13 @@ abstract class Protocol
}
// We are an IP node
$this->optionSet(self::O_TCP);
$this->client = $client;
// @todo This appears to be a bug in laravel? Need to call app()->isDownForMaintenance() twice?
app()->isDownForMaintenance();
$this->down = app()->isDownForMaintenance();
switch (get_class($this)) {
case EMSI::class:
switch ($mo->name) {
case 'EMSI':
Log::debug(sprintf('%s:- Starting EMSI',self::LOGKEY));
$this->optionSet(self::O_TCP);
$rc = $this->protocol_init();
if ($rc < 0) {
Log::error(sprintf('%s:! Unable to start EMSI [%d]',self::LOGKEY,$rc));
@@ -419,23 +382,21 @@ abstract class Protocol
break;
case Binkp::class:
case 'BINKP':
Log::debug(sprintf('%s:- Starting BINKP',self::LOGKEY));
$this->optionSet(self::O_TCP);
$rc = $this->protocol_session($this->originate);
break;
case DNS::class:
return $this->protocol_session();
default:
Log::error(sprintf('%s:! Unsupported session type [%s]',self::LOGKEY,get_class($this)));
Log::error(sprintf('%s:! Unsupported session type [%d]',self::LOGKEY,$mo->id));
return self::S_FAILURE;
}
// @todo Unlock outbounds
// @todo These flags determine when we connect to the remote.
// If the remote indicated that they dont support file requests (NRQ) or temporarily hold them (HRQ)
if (($this->node->optionGet(self::O_NRQ) && (! $this->setup->optionGet(EMSI::F_IGNORE_NRQ,'emsi_options'))) || $this->node->optionGet(self::O_HRQ))
@@ -465,9 +426,8 @@ abstract class Protocol
if ($so && $so->exists) {
foreach ($this->node->aka_other as $aka)
// @todo For disabled zones, we shouldnt refuse to record the address
// @todo If the system hasnt presented an address for a configured period (eg: 30 days) assume it no longer carries it
if ((! Address::findFTN($aka)) && ($oo=Address::createFTN($aka,$so))) {
if (! Address::findFTN($aka)) {
$oo = Address::createFTN($aka,$so);
$oo->validated = TRUE;
$oo->save();
}
@@ -478,7 +438,7 @@ abstract class Protocol
$slo->items_sent_size = $this->send->total_sent_bytes;
$slo->items_recv = $this->recv->total_recv;
$slo->items_recv_size = $this->recv->total_recv_bytes;
$slo->mailer_id = $this->mailer_id;
$slo->mailer_id = $mo->id;
$slo->sessiontime = $this->node->session_time;
$slo->result = ($rc & self::S_MASK);
$slo->originate = $this->originate;
@@ -492,6 +452,12 @@ abstract class Protocol
}
}
// @todo Optional after session execution event
// if ($this->node->start_time && $this->setup->cfg('CFG_AFTERSESSION')) {}
// @todo Optional after session includes mail event
// if ($this->node->start_time && $this->setup->cfg('CFG_AFTERMAIL')) {}
return $rc;
}

View File

@@ -11,9 +11,10 @@ use League\Flysystem\UnreadableFileEncountered;
use App\Classes\Crypt;
use App\Classes\Node;
use App\Classes\Protocol as BaseProtocol;
use App\Classes\Sock\Exception\SocketException;
use App\Classes\Sock\SocketClient;
use App\Classes\Sock\SocketException;
use App\Exceptions\{FileGrewException,InvalidFTNException};
use App\Models\{Address,Setup};
use App\Models\{Address,Mailer};
final class Binkp extends BaseProtocol
{
@@ -141,26 +142,36 @@ final class Binkp extends BaseProtocol
*/
private Crypt $crypt_out;
/**
* Incoming BINKP session
*
* @param SocketClient $client
* @return int|null
* @throws SocketException
*/
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(Mailer::where('name','BINKP')->singleOrFail(),$client,(new Address));
$this->client->close();
exit(0);
}
return NULL;
}
/**
* BINKD handshake
*
* @throws \Exception
*/
private function binkp_hs(): bool
private function binkp_hs(): void
{
Log::debug(sprintf('%s:+ Starting BINKP handshake',self::LOGKEY));
if (! $this->originate && $this->down) {
Log::info(sprintf('%s:! System down for maintenance',self::LOGKEY));
$this->msgs(self::BPM_BSY,'RETRY 0600: Down for maintenance, back soon...');
// @note Sometimes the remote drops the connection when we send the busy
while (($this->tx_left || $this->mqueue->count()) && $this->binkp_send()) {}
return FALSE;
}
if (! $this->originate && $this->capGet(self::F_MD,self::O_WANT)) {
$random_key = random_bytes(8);
$this->md_challenge = md5($random_key,TRUE);
@@ -173,7 +184,7 @@ final class Binkp extends BaseProtocol
$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',Setup::PRODUCT_NAME_SHORT,Setup::version(),self::PROT,self::VERSION));
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' : '';
@@ -195,8 +206,6 @@ final class Binkp extends BaseProtocol
$this->msgs(self::BPM_ADR,$addresses->pluck('ftn')->join(' '));
}
return TRUE;
}
/**
@@ -371,13 +380,11 @@ final class Binkp extends BaseProtocol
if ($this->capGet(self::F_CRYPT,self::O_YES)) {
Log::debug(sprintf('%s:%% Decrypting data from remote.',self::LOGKEY));
$this->rx_buf .= ($x=$this->crypt_in->decrypt($rx_buf));
$this->rx_buf .= $this->crypt_in->decrypt($rx_buf);
} else {
$this->rx_buf .= ($x=$rx_buf);
$this->rx_buf .= $rx_buf;
}
Log::debug(sprintf('%s:- We read [%d] chars from remote',self::LOGKEY,strlen($x)),['rx_buf'=>hex_dump($x)]);
}
Log::debug(sprintf('%s:- Read buffer has [%d] chars to process.',self::LOGKEY,strlen($this->rx_buf)));
@@ -414,7 +421,7 @@ final class Binkp extends BaseProtocol
}
if (static::DEBUG)
Log::debug(sprintf('%s:- rx_buf size [%d]',self::LOGKEY,strlen($this->rx_buf)));
Log::debug(sprintf('%s: - binkp_recv BUFFER [%d]',self::LOGKEY,strlen($this->rx_buf)));
$msg = ord(substr($this->rx_buf,0,1));
@@ -467,11 +474,6 @@ final class Binkp extends BaseProtocol
$rc = $this->M_get($data);
break;
case self::BPM_SKIP:
Log::debug(sprintf('%s:- SKIP:Remote requested to skip file [%s]',self::LOGKEY,$data));
$rc = $this->M_skip($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);
@@ -641,7 +643,7 @@ final class Binkp extends BaseProtocol
$offs = (int)$this->strsep($str,' ');
$flags = $this->strsep($str,' ');
if ($name && is_numeric($size) && $time) {
if ($name && $size && $time) {
return [
'file'=>['name'=>$name,'size'=>$size,'mtime'=>$time],
'offs'=>$offs,
@@ -696,7 +698,7 @@ final class Binkp extends BaseProtocol
// If we only present limited AKAs dont validate password against akas outside of the domains we present
} elseif (is_null(our_address($o))) {
Log::debug(sprintf('%s:/ AKA domain [%s] is not in our domain(s) [%s] - ignoring',self::LOGKEY,$o->zone->domain->name,our_address()->pluck('zone.domain.name')->unique()->join(',')));
Log::alert(sprintf('%s:/ AKA domain [%s] is not in our domain(s) [%s] - ignoring',self::LOGKEY,$o->zone->domain->name,our_address()->pluck('zone.domain.name')->unique()->join(',')));
$this->node->ftn_other = $rem_aka;
continue;
@@ -711,7 +713,6 @@ final class Binkp extends BaseProtocol
Log::info(sprintf('%s:- Got AKA [%s]',self::LOGKEY,$rem_aka));
// We'll update this address status
// @todo this shouldnt be here, since we havent authenticated the node
$o->validated = TRUE;
$o->role &= ~(Address::NODE_HOLD|Address::NODE_DOWN);
$o->save();
@@ -872,13 +873,16 @@ final class Binkp extends BaseProtocol
//if ($this->recv->fd)
// $this->recv->close();
// If we cannot understand the file, we'll send back a SKIP
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));
$this->msgs(self::BPM_SKIP,$buf);
return TRUE;
if ($this->sessionGet(self::SE_SENDFILE))
$this->send->close(FALSE,$this->node);
$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.
@@ -896,18 +900,6 @@ final class Binkp extends BaseProtocol
$this->recv->new($file['file'],$this->node->address,$this->force_queue);
// If the file is zero byte size, we'll skip it
if ($this->recv->recvsize === 0) {
Log::alert(sprintf('%s:! SKIPPING zero byte file info [%s]',self::LOGKEY,$this->recv->nameas));
$this->msgs(self::BPM_SKIP,$this->recv->name_size_time);
// Close the file, since we are skipping it.
$this->recv->close();
return TRUE;
}
try {
switch ($this->recv->open($file['offs']<0,$file['flags'])) {
case self::FOP_ERROR:
@@ -1001,46 +993,7 @@ final class Binkp extends BaseProtocol
}
/**
* M_SKIP commands
*
* @param string $buf
* @return bool
* @throws \Exception
* @todo We need to not add more files this session if a node skips a file
*/
private function M_skip(string $buf): bool
{
Log::alert(sprintf('%s:+ Remote request to skip the file for now [%s]',self::LOGKEY,$buf));
if ($file = $this->file_parse($buf)) {
if ($this->send->nameas
&& ! strncasecmp(Arr::get($file,'file.name'),$this->send->nameas,self::MAX_PATH)
&& $this->send->mtime === Arr::get($file,'file.mtime')
&& $this->send->size === Arr::get($file,'file.size'))
{
if ((! $this->sessionGet(self::SE_SENDFILE)) && (! $this->sessionGet(self::SE_WAITGOT))) {
Log::error(sprintf('%s:! M_skip for unknown file [%s]',self::LOGKEY,$buf));
} else {
Log::info(sprintf('%s:= Packet/File [%s], type [%d] skipped.',self::LOGKEY,$this->send->nameas,$this->send->type));
$this->sessionClear(self::SE_WAITGOT|self::SE_SENDFILE);
$this->send->close(FALSE,$this->node);
}
} else {
Log::error(sprintf('%s:! M_skip not for our file? [%s]',self::LOGKEY,$buf));
}
} else {
Log::error(sprintf('%s:! UNPARSABLE file info [%s]',self::LOGKEY,$buf));
}
return TRUE;
}
/**
* M_GOTSKIP command
* M_GOT/M_SKIP commands
*
* @param string $buf
* @return bool
@@ -1264,13 +1217,9 @@ final class Binkp extends BaseProtocol
}
}
if ($this->optionGet(self::O_PWD)) {
if ($this->optionGet(self::O_PWD))
Log::info(sprintf('%s:- SECURE',self::LOGKEY));
// @todo Since we have connected, if the node was marked down/hold reset that
// Notification::route('netmail',$ao->system->aka_unknown()->first()->withoutRelations())->notify(new NodeMarkedDownNetmail($ao->withoutRelations()));
}
return $this->binkp_hsdone();
}
@@ -1366,9 +1315,6 @@ final class Binkp extends BaseProtocol
if ($this->node->aka_authed) {
$this->msgs(self::BPM_OK,sprintf('%ssecure',$have_pwd ? '' : 'non-'));
// @todo Since we have connected, if the node was marked down/hold reset that
// Notification::route('netmail',$ao->system->aka_unknown()->first()->withoutRelations())->notify(new NodeMarkedDownNetmail($ao->withoutRelations()));
} else {
$this->msgs(self::OK,'non-secure');
}
@@ -1395,8 +1341,7 @@ final class Binkp extends BaseProtocol
return self::S_FAILURE;
$this->force_queue = $force_queue;
if (! $this->binkp_hs())
return self::S_FAILURE;
$this->binkp_hs();
while (TRUE) {
if ((! $this->sessionGet(self::SE_INIT))
@@ -1551,7 +1496,12 @@ final class Binkp extends BaseProtocol
Log::info(sprintf('%s:- We have authed these AKAs [%s]',self::LOGKEY,$node->aka_remote_authed->pluck('ftn')->join(',')));
foreach ($node->aka_remote_authed as $ao) {
Log::info(sprintf('%s:- Checking for any new mail and files to [%s]',self::LOGKEY,$ao->ftn));
Log::debug(sprintf('%s:- Checking for any new mail and files to [%s]',self::LOGKEY,$ao->ftn));
if (! $ao->validated) {
Log::alert(sprintf('%s:! Address [%s] is not validated, so we wont bundle mail for it',self::LOGKEY,$ao->ftn));
continue;
}
$this->send->mail($ao);
$this->send->files($ao);
@@ -1568,14 +1518,33 @@ final class Binkp extends BaseProtocol
*/
}
Log::info(sprintf('%s:- We have [%d] items to send to [%s]',self::LOGKEY,$this->send->togo_count,$ao->ftn));
Log::info(sprintf('%s:- We have [%d] items to send to [%s]',self::LOGKEY,$this->send->togo_count,$ao->system->name));
} else {
// @todo We should only send netmail if unauthenticated - netmail that is direct to this node (no routing)
Log::alert(sprintf('%s:- Not AUTHed so not looking for mail, but we know these akas [%s]',self::LOGKEY,$node->aka_remote->pluck('ftn')->join(',')));
Log::debug(sprintf('%s:- Not AUTHed so not looking for mail, but we know these akas [%s]',self::LOGKEY,$node->aka_remote->pluck('ftn')->join(',')));
}
}
/**
* Strip blanks at the beginning of a string
*
* @param string $str
* @return string
* @throws \Exception
* @deprecated - use ltrim instead
*/
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
*
@@ -1596,4 +1565,20 @@ final class Binkp extends BaseProtocol
return $return;
}
/**
* Check if the string is a space
*
* @param string $str
* @return bool
* @throws \Exception
* @deprecated No longer required since we are using ltrim
*/
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"]);
}
}

View File

@@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use App\Classes\Protocol as BaseProtocol;
use App\Classes\Sock\SocketClient;
use App\Models\{Address,Domain,Mailer};
/**
@@ -63,6 +64,22 @@ final class DNS extends BaseProtocol
public const DNS_TYPE_OPT = 41; // OPT Records
public const DNS_TYPE_DS = 43; // DS Records (Delegation signer RFC 4034)
public function onConnect(SocketClient $client): ?int
{
// If our parent returns a PID, we've forked
if (! parent::onConnect($client)) {
Log::withContext(['pid'=>getmypid()]);
$this->client = $client;
$this->protocol_session();
Log::info(sprintf('%s:= onConnect - Connection closed [%s]',self::LOGKEY,$client->address_remote));
exit(0);
}
return NULL;
}
protected function protocol_init(): int
{
// N/A
@@ -93,7 +110,7 @@ final class DNS extends BaseProtocol
$this->query = new BaseProtocol\DNS\Query($this->client->read(0,512));
} catch (\Exception $e) {
Log::notice(sprintf('%s:! Ignoring bad DNS query (%s)',self::LOGKEY,$e->getMessage()));
Log::error(sprintf('%s:! Ignoring bad DNS query (%s)',self::LOGKEY,$e->getMessage()));
return FALSE;
}
@@ -102,7 +119,7 @@ final class DNS extends BaseProtocol
// If the wrong class
if ($this->query->class !== self::DNS_QUERY_IN) {
Log::notice(sprintf('%s:! We only service Internet queries [%d]',self::LOGKEY,$this->query->class));
Log::error(sprintf('%s:! We only service Internet queries [%d]',self::LOGKEY,$this->query->class));
return $this->reply(self::DNS_NOTIMPLEMENTED,[],$this->soa());
}
@@ -150,7 +167,7 @@ final class DNS extends BaseProtocol
case self::DNS_TYPE_AAAA:
case self::DNS_TYPE_SRV:
case self::DNS_TYPE_TXT:
Log::debug(sprintf('%s:= Looking for record [%s] for [%s]',self::LOGKEY,$this->query->type,$this->query->domain));
Log::info(sprintf('%s:= Looking for record [%s] for [%s]',self::LOGKEY,$this->query->type,$this->query->domain));
$labels = clone($this->query->labels);
$mailer = '';
@@ -162,11 +179,11 @@ final class DNS extends BaseProtocol
switch ($labels->first()) {
case '_binkp':
$mailer = Mailer::where('name','BINKP')->sole();
$mailer = Mailer::where('name','BINKP')->singleOrFail();
break;
case '_ifcico':
$mailer = Mailer::where('name','EMSI')->sole();
$mailer = Mailer::where('name','EMSI')->singleOrFail();
break;
default:
@@ -215,15 +232,14 @@ final class DNS extends BaseProtocol
// Check we have the right record
if ((! $ao) || (($rootdn !== self::TLD) && ((! $ao->zone->domain->dnsdomain) || ($ao->zone->domain->dnsdomain !== $rootdn)))) {
Log::alert(sprintf('%s:= No DNS record for [%d:%d/%d.%d@%s]',self::LOGKEY,$z,$n,$f,$p,$d));
return $this->nameerr();
}
switch ($this->query->type) {
case self::DNS_TYPE_SRV:
if (($ao->system->address) && ($xx=$ao->system->mailers->where('id',$mailer->id)->pop())) {
Log::info(sprintf('%s:= Returning [%s] for DNS query [%s]',self::LOGKEY,$ao->system->address,$ao->ftn));
if (($ao->system->address) && ($xx=$ao->system->mailers->where('id',$mailer->id)->pop())) {
return $this->reply(
self::DNS_NOERROR,
[serialize([
@@ -234,8 +250,6 @@ final class DNS extends BaseProtocol
]) => self::DNS_TYPE_SRV]);
} else {
Log::alert(sprintf('%s:! No/incomplete hostname/port details for [%d] for DNS query [%s]',self::LOGKEY,$ao->system->id,$ao->ftn));
return $this->nodata();
}
@@ -247,7 +261,7 @@ final class DNS extends BaseProtocol
[serialize($ao->system->name) => self::DNS_TYPE_TXT]);
default:
Log::info(sprintf('%s:= Returning [%s] for DNS query [%s]',self::LOGKEY,$ao->system->address ?: 'NO ADDRESS',$ao->ftn));
Log::info(sprintf('%s:= Returning [%s] for DNS query [%s]',self::LOGKEY,$ao->system->address,$ao->ftn));
return (! $ao->system->address)
? $this->nodata()
@@ -258,7 +272,7 @@ final class DNS extends BaseProtocol
// Other attributes return NOTIMPL
default:
Log::notice(sprintf('%s:! We dont support DNS query types [%d]',self::LOGKEY,$this->query->type));
Log::error(sprintf('%s:! We dont support DNS query types [%d]',self::LOGKEY,$this->query->type));
return $this->reply(self::DNS_NOTIMPLEMENTED,[],$this->soa());
}
@@ -295,14 +309,14 @@ final class DNS extends BaseProtocol
private function nameerr(): int
{
Log::notice(sprintf('%s:! DNS query for a resource we dont manage [%s]',self::LOGKEY,$this->query->domain));
Log::error(sprintf('%s:! DNS query for a resource we dont manage [%s]',self::LOGKEY,$this->query->domain));
return $this->reply(self::DNS_NAMEERR,[],$this->soa());
}
private function nodata(): int
{
Log::notice(sprintf('%s:! DNS query for a resource we dont manage [%s] in our zone(s)',self::LOGKEY,$this->query->domain));
Log::error(sprintf('%s:! DNS query for a resource we dont manage [%s] in our zone(s)',self::LOGKEY,$this->query->domain));
return $this->reply(self::DNS_NOERROR,[],$this->soa());
}

View File

@@ -6,11 +6,12 @@ use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use App\Classes\Protocol as BaseProtocol;
use App\Classes\Sock\Exception\SocketException;
use App\Classes\Sock\SocketClient;
use App\Classes\Sock\SocketException;
use App\Exceptions\InvalidFTNException;
use App\Models\{Address,Mailer,Setup};
use App\Interfaces\CRC as CRCInterface;
use App\Interfaces\Zmodem as ZmodemInterface;
use App\Models\{Address,Setup};
use App\Traits\CRC as CRCTrait;
// http://ftsc.org/docs/fsc-0056.001
@@ -81,6 +82,27 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
'1'=>self::P_ZMODEM,
];
/**
* Incoming EMSI session
*
* @param SocketClient $client
* @return int|null
* @throws SocketException
*/
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(Mailer::where('name','EMSI')->singleOrFail(),$client,(new Address));
$this->client->close();
exit(0);
}
return NULL;
}
/**
* Send our welcome banner
*
@@ -185,8 +207,8 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
// Mailer Details
$makedata .= sprintf('{%s}{%s}{%s}{%s}',
Setup::product_id(),
Setup::PRODUCT_NAME_SHORT,
Setup::version(),
config('app.name'),
$this->setup->version,
'#000000' // Serial Numbers
);
@@ -908,135 +930,16 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
$this->client->rx_purge();
$this->client->tx_purge();
if ($this->down) {
Log::info(sprintf('%s:! System down for maintenance',self::LOGKEY));
$this->client->buffer_add(self::EMSI_NAK.'Sorry down for maintenance, call back again after a few minutes'.self::CR.self::CR);
$this->client->buffer_flush(5);
return -1;
}
$this->emsi_banner();
$t1 = $this->client->timer_set(self::EMSI_HSTIMEOUT);
$t2 = $this->client->timer_set(self::EMSI_RESEND_TO);
$c = 0;
while (! $this->client->timer_expired($t1)) {
try {
$ch = $this->client->read_ch(max( 1,min($this->client->timer_rest($t1),$this->client->timer_rest($t2))));
} catch (SocketException $e) {
if ($c++ > 2)
return self::TIMEOUT;
else
$ch = -2;
}
if (static::DEBUG)
Log::debug(sprintf('%s:- Got [%x] (%c)',self::LOGKEY,$ch,$ch));
// Look for Telnet IAC, if binary mode we'll need to handle IAC IAC => IAC
if ($ch === 0xff) {
Log::info(sprintf('%s:- TELNET IAC',self::LOGKEY));
$iaccmd = NULL;
// Peek for the next chars
do {
try {
$iac = $this->client->read(1,1,MSG_PEEK);
if (static::DEBUG)
Log::debug(sprintf('%s: - IAC LOOP',self::LOGKEY),['iac'=>ord($iac),'cmd'=>$iaccmd]);
switch (ord($iac)) {
// Binary Mode
case 0x00:
if ($iaccmd === 0xfb) {
Log::debug(sprintf('%s: - IAC WILL BINARY [%02x]',self::LOGKEY,ord($iac)));
// Config with DO
$this->client->send(chr(0xff).chr(0xfd).$iac,10);
} elseif ($iaccmd === 0xfd) {
Log::debug(sprintf('%s: - IAC DO BINARY [%02x]',self::LOGKEY,ord($iac)));
// Config with WILL
if (! $this->client->iac_bin) {
$this->client->send(chr(0xff).chr(0xfb).$iac,10);
$this->client->iac_bin = true;
}
}
$iaccmd = NULL;
break;
// Suppress Go Ahead
case 0x03:
if ($iaccmd === 0xfb) {
Log::debug(sprintf('%s: - IAC WILL SUPPRESS-GO-AHEAD [%02x]',self::LOGKEY,ord($iac)));
// Config with DO
$this->client->send(chr(0xff).chr(0xfd).$iac,10);
} elseif ($iaccmd === 0xfd) {
Log::debug(sprintf('%s: - IAC DO SUPPRESS-GO-AHEAD [%02x]',self::LOGKEY,ord($iac)));
// Config with WILL
$this->client->send(chr(0xff).chr(0xfb).$iac,10);
}
$iaccmd = NULL;
break;
// Will
case 0xfb:
if (static::DEBUG)
Log::debug(sprintf('%s: - IAC WILL [%02x]',self::LOGKEY,ord($iac)));
$iaccmd = ord($iac);
break;
// Do
case 0xfd:
if (static::DEBUG)
Log::debug(sprintf('%s: - IAC DO [%02x]',self::LOGKEY,ord($iac)));
$iaccmd = ord($iac);
break;
// IAC
case 0xff:
if (static::DEBUG)
Log::debug(sprintf('%s: - IAC [%02x]',self::LOGKEY,ord($iac)));
$iaccmd = ord($iac);
break;
default:
Log::alert(sprintf('%s: - IAC Unhandled [%02x]',self::LOGKEY,ord($iac)),['iac'=>$iac,'iaccmd'=>$iaccmd,'ch'=>ord($iac)]);
$ch = ord($iac);
$iac = NULL;
}
if ($iaccmd) {
$iac = ord($this->client->read_ch(10));
$ch = NULL;
} elseif (is_null($ch)) {
$ch = ord($this->client->read_ch(10));
}
} catch (SocketException $e) {
Log::debug(sprintf('%s:! SocketException: %s',self::LOGKEY,$e->getMessage()),['class'=>get_class($e),'code'=>$e->getCode()]);
$iac = NULL;
}
} while (! is_null($iac));
Log::debug(sprintf('%s:- Leaving IAC with [%02x]',self::LOGKEY,$ch),['ch'=>serialize($ch)]);
}
if (($ch != self::TIMEOUT) && ($ch < 0))
return $ch;
@@ -1150,9 +1053,6 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
return (self::S_REDIAL|self::S_ADDTRY);
}
// @todo Since we have connected, if the node was marked down/hold reset that
// Notification::route('netmail',$ao->system->aka_unknown()->first()->withoutRelations())->notify(new NodeMarkedDownNetmail($ao->withoutRelations()));
// @todo Lock Node AKAs
Log::info(sprintf('%s:- We have [%lu%s] mail, [%lu%s] files',self::LOGKEY,$this->send->mail_size,'b',$this->send->files_size,'b'));
@@ -1304,7 +1204,7 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
Log::debug(sprintf('%s:+ Start WAZOO Receive',self::LOGKEY));
// @todo If the node is not defined in the DB node->address is NULL. Need to figure out how to handle those nodes.
$rc = (new Zmodem($this->setup))->zmodem_receive($this->client,$zap,$this->recv,$this->node->address,$this->force_queue);
$rc = (new Zmodem)->zmodem_receive($this->client,$zap,$this->recv,$this->node->address,$this->force_queue);
return ($rc === self::RCDO || $rc === self::ERROR);
}
@@ -1326,9 +1226,14 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
// Add our mail to the queue if we have authenticated
if ($this->node->aka_authed)
foreach ($this->node->aka_remote_authed as $ao) {
if (! $ao->validated) {
Log::alert(sprintf('%s:! Address [%s] is not validated, so we wont bundle mail for it',self::LOGKEY,$ao->ftn));
continue;
}
// Send mail
while ($this->send->mail($ao)) {
$z = new Zmodem($this->setup);
$z = new Zmodem;
if (! $z->zmodem_sendinit($this->client,$zap) && $this->send->togo_count)
$z->zmodem_sendfile($this->send,$this->node);
@@ -1336,7 +1241,7 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
// Send files
while ($this->send->files($ao)) {
$z = new Zmodem($this->setup);
$z = new Zmodem;
if (! $z->zmodem_sendinit($this->client,$zap) && $this->send->togo_count)
$z->zmodem_sendfile($this->send,$this->node);

View File

@@ -5,12 +5,12 @@ namespace App\Classes\Protocol;
use Illuminate\Support\Facades\Log;
use App\Classes\{Node,Protocol};
use App\Classes\Protocol\Zmodem as ZmodemClass;
use App\Classes\File\{Receive,Send};
use App\Classes\Sock\Exception\SocketException;
use App\Classes\Sock\SocketClient;
use App\Classes\Sock\{SocketClient,SocketException};
use App\Interfaces\CRC as CRCInterface;
use App\Interfaces\Zmodem as ZmodemInterface;
use App\Models\Address;
use App\Models\{Address,Mailer};
use App\Traits\CRC as CRCTrait;
/**
@@ -202,6 +202,27 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
private string $rxbuf = '';
private string $txbuf = '';
/**
* @param SocketClient $client
* @return null
* @throws SocketException
*/
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(Mailer::where('name','ZMODEM')->singleOrFail(),$client);
$this->client->close();
Log::info(sprintf('%s:= onConnect - Connection closed [%s]',self::LOGKEY,$client->address_remote));
exit(0);
}
return NULL;
}
/**
* Initialise our session
*/
@@ -489,7 +510,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
* @param Send $send
* @return int
*/
public function zmodem_sendfile(Send $send,Node $node): void
public function zmodem_sendfile(Send $send,Node $node): int
{
Log::debug(sprintf('%s:+ zmodem_sendfile',self::LOGKEY));
@@ -513,16 +534,14 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
break;
}
return;
return $rc;
} catch (\Exception $e) {
Log::error(sprintf('%s:! Error [%s]',self::LOGKEY,$e->getMessage()),['rc'=>$rc ?? '-UNDEFINED-']);
return;
Log::error(sprintf('%s:! Error [%s]',self::LOGKEY,$e->getMessage()));
}
}
return;
return self::OK;
}
/**
@@ -1147,7 +1166,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
private function ls_zrecvdata32(string &$data,int &$len,int $timeout): int
{
if (static::DEBUG)
Log::debug(sprintf('%s:+ ls_zrecvdata32',self::LOGKEY),['d'=>$data,'len'=>$len,'timeout'=>$timeout]);
Log::debug(sprintf('%s:+ ls_zrecvdata32',self::LOGKEY),['d'=>$data]);
$got = 0; /* Bytes total got */
$crc = 0; /* Received CRC */
@@ -1165,9 +1184,6 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
return self::LSZ_BADCRC;
} else {
if (static::DEBUG)
Log::debug(sprintf('%s:- ls_zrecvdata32 c>32 [%x] (%c)',self::LOGKEY,$c,($c<31 ? 32 : $c)),['c'=>serialize($c)]);
switch ($c) {
case self::LSZ_CRCE:
case self::LSZ_CRCG:
@@ -1280,8 +1296,6 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
break;
case self::LSZ_BADCRC:
$this->rxbuf = '';
case self::TIMEOUT:
if ($this->ls_rxAttnStr) {
$this->client->buffer_add($this->ls_rxAttnStr);
@@ -1310,9 +1324,6 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
$needzdata = 1;
}
if (static::DEBUG)
Log::debug(sprintf('%s:- ls_zrecvfile RC [%s]',self::LOGKEY,$rc),['needzdata'=>$needzdata]);
/* We need new position -- ZDATA (and may be ZEOF) */
} else {
Log::debug(sprintf('%s:- ls_zrecvfile Want ZDATA/ZEOF at [%d]',self::LOGKEY,$rxpos));
@@ -1343,7 +1354,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
return self::OK;
}
Log::debug(sprintf('%s:- ls_zrecvfile ZDATA',self::LOGKEY),['newpos'=>$newpos]);
Log::debug(sprintf('%s:- ls_zrecvfile ZDATA',self::LOGKEY));
$needzdata = 0;
}
}
@@ -1936,9 +1947,6 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
$this->ls_zsendhhdr(self::ZNAK,$this->ls_storelong(0));
}
// sleep between tries
sleep(5);
} while (++$trys < 10);
Log::error(sprintf('%s:? ls_zrecvnewpos Something strange or timeout [%d]',self::LOGKEY,$rc));

View File

@@ -1,6 +0,0 @@
<?php
namespace App\Classes\Sock\Exception;
final class HAproxyException extends \Exception {
}

View File

@@ -6,8 +6,6 @@ use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use App\Classes\Sock\Exception\{HAproxyException,SocketException};
/**
* Class SocketClient
*
@@ -48,8 +46,7 @@ final class SocketClient {
/** @var string Data in the RX buffer */
private string $rx_buf = '';
public function __construct (\Socket $connection,bool $originate=FALSE)
{
public function __construct (\Socket $connection) {
$this->connection = $connection;
if ($this->type === SOCK_STREAM) {
@@ -57,79 +54,42 @@ final class SocketClient {
socket_getpeername($connection,$this->address_remote,$this->port_remote);
// If HAPROXY is used, work get the clients address
if ((! $originate) && config('fido.haproxy')) {
if (config('fido.haproxy')) {
Log::debug(sprintf('%s:+ HAPROXY connection host [%s] on port [%d] (%s)',self::LOGKEY,$this->address_remote,$this->port_remote,$this->type));
if (($x=$this->read(5,6)) === 'PROXY ')
$vers = 1;
elseif (($x === "\x0d\x0a\x0d\x0a\x00\x0d") && ($this->read(5,6) === "\x0aQUIT\x0a"))
$vers = 2;
else
throw new HAproxyException('Failed to initialise HAPROXY connection');
switch ($vers) {
case 1:
// Protocol/Address Family
switch ($x=$this->read(5,5)) {
case 'TCP4 ':
$p = 4;
break;
case 'TCP6 ':
$p = 6;
break;
default:
throw new HAproxyException(sprintf('HAPROXY protocol [%d] is not handled',$x));
if ($this->read(5,12) !== "\x0d\x0a\x0d\x0a\x00\x0d\x0aQUIT\x0a") {
Log::error(sprintf('%s:! Failed to initialise HAPROXY connection',self::LOGKEY));
throw new SocketException(SocketException::CANT_CONNECT,'Failed to initialise HAPROXY connection');
}
$read = $this->read(5,104-11);
// IPv4
if (($p === 4) || ($p === 6)) {
$parse = collect(sscanf($read,'%s %s %s %s'));
$src = Arr::get($parse,0);
$dst = Arr::get($parse,1);
$src_port = (int)Arr::get($parse,2);
$dst_port = (int)Arr::get($parse,3);
$len = $parse->map(fn($item)=>strlen($item))->sum()+3;
// The last 2 chars should be "\r\n"
if (($x=substr($read,$len)) !== "\r\n")
throw new HAproxyException(sprintf('HAPROXY parsing failed for version [%d] [%s] (%s)',$p,$read,hex_dump($x)));
} else {
throw new HAproxyException(sprintf('HAPROXY version [%d] is not handled [%s]',$p,$read));
}
$this->port_remote = $src_port;
break;
case 2:
// Version/Command
$vc = $this->read_ch(5);
if (($x=($vc>>4)&0x7) !== 2)
throw new HAproxyException(sprintf('Unknown HAPROXY version [%d]',$x));
if (($x=($vc>>4)&0x7) !== 2) {
Log::error(sprintf('%s:! HAPROXY version [%d] is not handled',self::LOGKEY,$x));
throw new SocketException(SocketException::CANT_CONNECT,'Unknown HAPROXY version');
}
switch ($x=($vc&0x7)) {
// HAPROXY internal
case 0:
throw new HAproxyException('HAPROXY internal health-check');
Log::debug(sprintf('%s:! HAPROXY internal health-check',self::LOGKEY));
throw new SocketException(SocketException::CANT_CONNECT,'Healthcheck');
// PROXY connection
case 1:
break;
default:
throw new HAproxyException(sprintf('HAPROXY command [%d] is not handled',$x));
Log::error(sprintf('%s:! HAPROXY command [%d] is not handled',self::LOGKEY,$x));
throw new SocketException(SocketException::CANT_CONNECT,'Unknown HAPROXY command');
}
// Protocol/Address Family
$pa = $this->read_ch(5);
$p = NULL;
switch ($x=($pa>>4)&0x7) {
case 1: // AF_INET
@@ -140,7 +100,9 @@ final class SocketClient {
$p = 6;
break;
default:
Log::error(sprintf('%s:! HAPROXY protocol [%d] is not handled',self::LOGKEY,$x));
throw new SocketException(SocketException::CANT_CONNECT,'Unknown HAPROXY protocol');
}
switch ($x=($pa&0x7)) {
@@ -148,7 +110,8 @@ final class SocketClient {
break;
default:
throw new HAproxyException(sprintf('HAPROXY address family [%d] is not handled',$x));
Log::error(sprintf('%s:! HAPROXY address family [%d] is not handled',self::LOGKEY,$x));
throw new SocketException(SocketException::CANT_CONNECT,'Unknown HAPROXY address family');
}
$len = Arr::get(unpack('n',$this->read(5,2)),1);
@@ -163,57 +126,61 @@ final class SocketClient {
$dst = inet_ntop($this->read(5,16));
} else {
throw new HAproxyException(sprintf('HAPROXY address len [%d:%d] is not handled',$p,$len));
Log::error(sprintf('%s:! HAPROXY address len [%d:%d] is not handled',self::LOGKEY,$p,$len));
throw new SocketException(SocketException::CANT_CONNECT,'Unknown HAPROXY address length');
}
$src_port = unpack('n',$this->read(5,2));
$dst_port = Arr::get(unpack('n',$this->read(5,2)),1);
$this->port_remote = Arr::get($src_port,1);
break;
default:
throw new HAproxyException('Failed to initialise HAPROXY connection');
}
$dst_port = unpack('n',$this->read(5,2));
$this->address_remote = $src;
$this->port_remote = Arr::get($src_port,1);
Log::debug(sprintf('%s:- HAPROXY src [%s:%d] dst [%s:%d]',
Log::info(sprintf('%s:! HAPROXY src [%s:%d] dst [%s:%d]',
self::LOGKEY,
$this->address_remote,
$this->port_remote,
$dst,
$dst_port,
Arr::get($dst_port,1),
));
}
Log::debug(sprintf('%s:+ Connection host [%s] on port [%d] (%s)',self::LOGKEY,$this->address_remote,$this->port_remote,$this->type));
Log::info(sprintf('%s:+ Connection host [%s] on port [%d] (%s)',self::LOGKEY,$this->address_remote,$this->port_remote,$this->type));
}
}
public function __get(string $key): mixed
{
return match ($key) {
'address_remote', 'port_remote' => $this->{$key},
'cps', 'speed' => Arr::get($this->session,$key),
'iac_bin' => Arr::get($this->session,$key),
'rx_free' => self::RX_BUF_SIZE-$this->rx_left,
'rx_left' => strlen($this->rx_buf),
'tx_free' => self::TX_BUF_SIZE-strlen($this->tx_buf),
'type' => socket_get_option($this->connection,SOL_SOCKET,SO_TYPE),
default => throw new \Exception(sprintf('%s:! Unknown key [%s]:',self::LOGKEY, $key)),
};
public function __get($key) {
switch ($key) {
case 'address_remote':
case 'port_remote':
return $this->{$key};
case 'cps':
case 'speed':
return Arr::get($this->session,$key);
case 'rx_free':
return self::RX_BUF_SIZE-$this->rx_left;
case 'rx_left':
return strlen($this->rx_buf);
case 'tx_free':
return self::TX_BUF_SIZE-strlen($this->tx_buf);
case 'type':
return socket_get_option($this->connection,SOL_SOCKET,SO_TYPE);
default:
throw new \Exception(sprintf('%s:! Unknown key [%s]:',self::LOGKEY,$key));
}
}
public function __set(string $key,mixed $value): void
{
public function __set($key,$value) {
switch ($key) {
case 'cps':
case 'speed':
case 'iac_bin':
$this->session[$key] = $value;
break;
return $this->session[$key] = $value;
default:
throw new \Exception(sprintf('%s:! Unknown key [%s]:',self::LOGKEY,$key));
@@ -226,14 +193,13 @@ final class SocketClient {
* @param string $address
* @param int $port
* @return static
* @throws SocketException|HAproxyException
* @throws SocketException
*/
public static function create(string $address,int $port): self
{
Log::info(sprintf('%s:+ Creating connection to [%s:%d]',self::LOGKEY,$address,$port));
$type = collect(config('fido.ip'))
->filter(fn($item)=>$item['enabled']);
$sort = collect(['AAAA','A']);
if (filter_var($address,FILTER_VALIDATE_IP))
$resolved = collect([[
@@ -242,15 +208,14 @@ final class SocketClient {
]]);
else
// We only look at AAAA/A records
$resolved = collect(dns_get_record($address,$type->map(fn($item)=>$item['type'])->sum()))
->filter(fn($item)=>$type->has(Arr::get($item,'type')))
->sort(fn($a,$b)=>$type->get(Arr::get($a,'type'))['order'] < $type->get(Arr::get($b,'type'))['order']);
$resolved = collect(dns_get_record($address,DNS_AAAA|DNS_A))
->filter(function($item) use ($sort) { return $sort->search(Arr::get($item,'type')) !== FALSE; })
->sort(function($item) use ($sort) { return $sort->search(Arr::get($item,'type')); });
if (! $resolved->count())
throw new SocketException(SocketException::CANT_CONNECT,sprintf('%s doesnt resolved to an IPv4/IPv6 address',$address));
$result = FALSE;
$socket = NULL;
foreach ($resolved as $address) {
try {
@@ -280,7 +245,7 @@ final class SocketClient {
if ($result === FALSE)
throw new SocketException(SocketException::CANT_CONNECT,socket_strerror(socket_last_error($socket)));
return new self($socket,TRUE);
return new self($socket);
}
/**
@@ -341,7 +306,7 @@ final class SocketClient {
while (strlen($this->tx_buf)) {
$tv = $this->timer_rest($tm);
if ($rc=$this->canSend($tv)) {
if (($rc=$this->canSend($tv)) > 0) {
if (self::DEBUG)
Log::debug(sprintf('%s:- Chars to send [%d]',self::LOGKEY,strlen($this->tx_buf)));
@@ -369,14 +334,14 @@ final class SocketClient {
/**
* @param int $timeout
* @return bool
* @return int
* @throws \Exception
*/
public function canSend(int $timeout): bool
public function canSend(int $timeout): int
{
$write = [$this->connection];
return $this->socketSelect(NULL,$write,NULL,$timeout) > 0;
return $this->socketSelect(NULL,$write,NULL,$timeout);
}
/**
@@ -396,21 +361,21 @@ final class SocketClient {
Log::error(sprintf('%s:! Closing socket [%s]',self::LOGKEY,$e->getMessage()));
}
Log::debug(sprintf('%s:= Connection closed with [%s]',self::LOGKEY,$this->address_remote));
Log::info(sprintf('%s:= Connection closed with [%s]',self::LOGKEY,$this->address_remote));
}
/**
* We have data in the buffer or on the socket
*
* @param int $timeout
* @return bool
* @return int
* @throws \Exception
*/
public function hasData(int $timeout): bool
public function hasData(int $timeout): int
{
$read = [$this->connection];
return ($this->rx_left ?: $this->socketSelect($read,NULL,NULL,$timeout)) > 0;
return $this->rx_left ?: $this->socketSelect($read,NULL,NULL,$timeout);
}
/**
@@ -418,11 +383,10 @@ final class SocketClient {
*
* @param int $timeout How long to wait for data
* @param int $len The amount of data we want
* @param int $flags
* @return string|null
* @throws SocketException
*/
public function read(int $timeout,int $len=1024,int $flags=MSG_DONTWAIT): ?string
public function read(int $timeout,int $len=1024): ?string
{
// We have data in our buffer
if ($this->rx_left >= $len) {
@@ -430,58 +394,24 @@ final class SocketClient {
Log::debug(sprintf('%s:- Returning [%d] chars from the RX buffer',self::LOGKEY,$len));
$result = substr($this->rx_buf,0,$len);
if ($flags !== MSG_PEEK)
$this->rx_buf = substr($this->rx_buf,strlen($result));
// In case we are in Telnet Binary Mode
if ($this->iac_bin) {
if (self::DEBUG)
Log::debug(sprintf('%s:- Telnet IAC Binary Mode, looking for ff ff',self::LOGKEY),['result'=>hex_dump($result)]);
// if the last char is ff, we need to get the next char
if (str_ends_with($result,"\xff")) {
if (self::DEBUG)
Log::debug(sprintf('%s: - We have a hit',self::LOGKEY));
// If we have it in our buffer, just get it
if ($this->rx_left) {
$result .= substr($this->rx_buf,0,1);
$this->rx_buf = substr($this->rx_buf,1);
// Else put everything back into rx_buf, and increase len by 1
} else {
$this->rx_buf = $result;
$len++;
$result = '';
}
}
if (strlen($result) > 1)
$result = str_replace("\xff\xff","\xff",$result);
if (strlen($result))
return $result;
} else
return $result;
}
if (self::DEBUG)
Log::debug(sprintf('%s:- Buffer doesnt have [%d] chars, it only has [%d], or it ends with 0xff',self::LOGKEY,$len,strlen($this->rx_buf)),['rx_buf'=>hex_dump($this->rx_buf)]);
if ($timeout && (! $this->hasData($timeout)))
throw new SocketException(SocketException::SOCKET_TIMEOUT,$timeout);
if ($timeout AND ($this->hasData($timeout) === 0))
return NULL;
$buf = '';
try {
switch ($this->type) {
case SOCK_STREAM:
$recv = socket_recv($this->connection,$buf,self::RX_SIZE,$flags);
$recv = socket_recv($this->connection,$buf,self::RX_SIZE,MSG_DONTWAIT);
break;
case SOCK_DGRAM:
$recv = socket_recvfrom($this->connection,$buf,self::RX_SIZE,$flags,$this->address_remote,$this->port_remote);
$recv = socket_recvfrom($this->connection,$buf,self::RX_SIZE,MSG_DONTWAIT,$this->address_remote,$this->port_remote);
break;
default:
@@ -522,19 +452,13 @@ final class SocketClient {
}
}
if ($flags === MSG_PEEK) {
Log::debug(sprintf('%s:- Returning [%d] chars as a result of a PEEK operation, buffer would have [%d], but still has [%d]',self::LOGKEY,$len,strlen($this->rx_buf.$buf),strlen($this->rx_buf)),['rx_buf'=>hex_dump($this->rx_buf),'buf'=>hex_dump($buf)]);
return substr($this->rx_buf.$buf,0,$len);
}
$this->rx_buf .= $buf;
if (self::DEBUG)
Log::debug(sprintf('%s:- Added [%d] chars to the RX buffer',self::LOGKEY,strlen($buf)),['rx_buf'=>hex_dump($this->rx_buf)]);
// Loop again and return the data, now that it is in the RX buffer
return $this->read($timeout,$len,$flags);
return $this->read($timeout,$len);
}
/**
@@ -547,14 +471,12 @@ final class SocketClient {
*/
public function read_ch(int $timeout): int
{
if ($this->hasData($timeout))
if ($this->hasData($timeout) > 0) {
$ch = $this->read($timeout,1);
else
throw new SocketException(SocketException::SOCKET_TIMEOUT,$timeout);
if (self::DEBUG)
Log::debug(sprintf('%s:+ read_ch [%c] (%x)',self::LOGKEY,$ch,ord($ch)));
} else {
return self::TIMEOUT;
}
return ord($ch);
}
@@ -583,22 +505,17 @@ final class SocketClient {
*
* @param string $message
* @param int $timeout
* @return int|bool
* @return int|false
* @throws \Exception
*/
public function send(string $message,int $timeout): int|bool
public function send(string $message,int $timeout): int|false
{
if ($timeout && (! $rc=$this->canSend($timeout)))
if ($timeout AND (! $rc=$this->canSend($timeout)))
return $rc;
if (self::DEBUG)
Log::debug(sprintf('%s:- Sending [%d] chars [%s]',self::LOGKEY,strlen($message),Str::limit($message,15)));
if ($this->iac_bin) {
Log::debug(sprintf('%s:- IAC_BIN mode, looking for 0xff',self::LOGKEY));
$message = str_replace("\xff","\xff\xff",$message);
}
switch ($this->type) {
case SOCK_STREAM:
return socket_write($this->connection,$message,strlen($message));

View File

@@ -1,6 +1,6 @@
<?php
namespace App\Classes\Sock\Exception;
namespace App\Classes\Sock;
// @todo Can we change this to use socket_strerr() && socket_last_error()
final class SocketException extends \Exception {
@@ -11,7 +11,6 @@ final class SocketException extends \Exception {
public const CANT_CONNECT = 5;
public const SOCKET_ERROR = 6;
public const SOCKET_EAGAIN = 11;
public const SOCKET_TIMEOUT = 15;
public const SOCKET_READ = 22;
public const CONNECTION_RESET = 104;
@@ -23,7 +22,6 @@ final class SocketException extends \Exception {
self::CANT_CONNECT => 'Can\'t connect: "%s"',
self::SOCKET_ERROR => 'Socket Error: "%s"',
self::SOCKET_EAGAIN => 'Socket Resource Temporarily Unavailable - Try again',
self::SOCKET_TIMEOUT => 'Timeout reached "%d"',
self::SOCKET_READ => 'Unable to read from socket',
self::CONNECTION_RESET => 'Connection reset by peer',
];

View File

@@ -4,8 +4,6 @@ namespace App\Classes\Sock;
use Illuminate\Support\Facades\Log;
use App\Classes\Sock\Exception\{HAproxyException,SocketException};
final class SocketServer {
private const LOGKEY = 'SS-';
@@ -127,27 +125,16 @@ final class SocketServer {
if (($accept = socket_accept($this->server)) === FALSE)
throw new SocketException(SocketException::CANT_ACCEPT,socket_strerror(socket_last_error($this->server)));
Log::debug(sprintf('%s:* TCP Loop Start',self::LOGKEY));
try {
$r = new SocketClient($accept);
} catch (HAproxyException $e) {
Log::notice(sprintf('%s:! HAPROXY Exception [%s]',self::LOGKEY,$e->getMessage()));
socket_close($accept);
continue;
} catch (\Exception $e) {
Log::notice(sprintf('%s:! Creating Socket client failed? [%s]',self::LOGKEY,$e->getMessage()));
Log::error(sprintf('%s:! Creating Socket client failed? [%s]',self::LOGKEY,$e->getMessage()));
socket_close($accept);
continue;
}
// If the handler returns a value, then that is the main thread
if (! $this->handler[0]->{$this->handler[1]}($r)) {
$r->close();
exit(0);
}
$this->handler[0]->{$this->handler[1]}($r);
}
}
@@ -157,8 +144,7 @@ final class SocketServer {
$r = new SocketClient($this->server);
if ($r->hasData(30)) {
if (! ($this->handler[0]->{$this->handler[1]}($r)))
exit(0);
$this->handler[0]->{$this->handler[1]}($r);
// Sleep so our thread has a chance to pick up the data from our connection
usleep(50000);

View File

@@ -1,42 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Jobs\AddressClearQueue as Job;
use App\Models\Address;
class AddressClearQueue extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'address:clear:queue'
.' {ftn : FTN}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clear up anything queued for an FTN';
/**
* Execute the console command.
*/
public function handle(): int
{
$ao = Address::findFTN($this->argument('ftn'),TRUE,TRUE);
if (! $ao) {
$this->error('FTN not found: '.$this->argument('ftn'));
return self::FAILURE;
}
return Job::dispatchSync($ao);
}
}

View File

@@ -30,7 +30,7 @@ class AddressIdle extends Command
*/
public function handle(): int
{
$do = Domain::where('name',$this->argument('domain'))->sole();
$do = Domain::where('name',$this->argument('domain'))->singleOrFail();
return Job::dispatchSync($do,$this->option('ftn') ? Address::findFTN($this->option('ftn')) : NULL);
}

View File

@@ -2,10 +2,10 @@
namespace App\Console\Commands\Areafix;
use Carbon\Carbon;
use Illuminate\Console\Command;
use App\Jobs\AreafixRescan;
use App\Models\{Address,Echoarea};
use App\Models\{Address,Echoarea,Echomail};
class Rescan extends Command
{
@@ -14,13 +14,7 @@ class Rescan extends Command
*
* @var string
*/
protected $signature = 'areafix:rescan'
.' {ftn : FTN Address}'
.' {area : Echoarea Tag}'
.' {days? : Limit to messages authored days ago}'
.' {--j|queue : Queue the Job}'
.' {--Q|queuename=default : Queue on queue}'
.' {--R|export : Re-export previously sent messages}';
protected $signature = 'areafix:rescan {ftn} {area} {days?}';
/**
* The console command description.
@@ -49,15 +43,50 @@ class Rescan extends Command
if (! $this->argument('area'))
throw new \Exception('Areaname is required');
$eo = Echoarea::where('name',$this->argument('area'))->sole();
$eao = Echoarea::where('name',$this->argument('area'))->singleOrFail();
if ($eao->domain_id !== $ao->zone->domain_id)
throw new \Exception(sprintf('Echo area [%s] is not in domain [%s] for FTN [%s]',$eao->name,$ao->zone->domain->name,$ao->ftn));
if ($eo->domain_id !== $ao->zone->domain_id)
throw new \Exception(sprintf('Echo area [%s] is not in domain [%s] for FTN [%s]',$eo->name,$ao->zone->domain->name,$ao->ftn));
// Check that the user is subscribed
if (! $ao->echoareas->contains($eao->id))
throw new \Exception(sprintf('FTN [%s] is not subscribed to [%s]',$ao->ftn,$eao->name));
if ($this->option('queue'))
AreafixRescan::dispatch($ao,$eo,$this->argument('days'))->onQueue($this->option('queuename'));
else
AreafixRescan::dispatchSync($ao,$eo,$this->argument('days'));
// Check that an FTN can read the area
if (! $eao->can_read($ao->security))
throw new \Exception(sprintf('FTN [%s] doesnt have permission to receive [%s]',$ao->ftn,$eao->name));
foreach (Echomail::select('id')
->where('echoarea_id',$eao->id)
->when($this->argument('days'),function($query) {
return $query->where('created_at','>=',Carbon::now()->subDays($this->argument('days'))->startOfDay());
})
->orderBy('datetime')
->cursor() as $eo) {
// Echomail hasnt been exported before
if (! $eo->seenby->count()) {
$eo->seenby()->attach($ao->id,['export_at'=>Carbon::now()]);
$this->info(sprintf('Exported [%d] to [%s]',$eo->id,$ao->ftn3d));
} else {
$export = $eo->seenby->where('id',$ao->id)->pop();
// Echomail is pending export
if ($export && $export->pivot->export_at && is_null($export->pivot->sent_at) && is_null($export->pivot->sent_pkt)) {
$this->warn(sprintf('Not exporting [%d] already queued for [%s]',$eo->id,$ao->ftn3d));
// Echomail has been exported
} elseif ($export) {
$eo->seenby()->updateExistingPivot($ao,['export_at'=>Carbon::now(),'sent_at'=>NULL,'sent_pkt'=>NULL]);
$this->info(sprintf('Re-exported [%d] to [%s]',$eo->id,$ao->ftn3d));
// Echomail has not been exported
} else {
$eo->seenby()->attach($ao,['export_at'=>Carbon::now(),'sent_at'=>NULL,'sent_pkt'=>NULL]);
$this->info(sprintf('Exported [%d] to [%s]',$eo->id,$ao->ftn3d));
}
}
}
return self::SUCCESS;
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Console\Commands\BBS;
use Illuminate\Console\Command;
use App\Classes\ANSI;
class ANSIDecode extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bbs:ansi:decode'
.' {file : ANS file to decode}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Decode ANS file from custom binary';
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
echo ANSI::ansi($this->argument('file'));
return self::SUCCESS;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Console\Commands\BBS;
use Illuminate\Console\Command;
use App\Classes\ANSI;
class ANSIEncode extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bbs:ansi:encode'
.' {file : ANS file to encode}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Encode ANS file to custom binary';
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
foreach (ANSI::binary($this->argument('file')) as $line) {
foreach (str_split(bin2hex($line),2) as $y)
echo hex2bin($y);
echo "\r";
}
return self::SUCCESS;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Console\Commands\BBS;
use App\Models\Frame;
use Illuminate\Console\Command;
class FrameDelete extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bbs:frame:delete {frame} {index}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete frames from the database.';
/**
* Execute the console command.
*
* @return int
* @throws \Exception
*/
public function handle(): int
{
if (! is_numeric($this->argument('frame')))
throw new \Exception('Frame is not numeric: '.$this->argument('frame'));
if (strlen($this->argument('index')) != 1 OR ! preg_match('/^[a-z]$/',$this->argument('index')))
throw new \Exception('Subframe failed validation');
try {
$o = Frame::where('frame',$this->argument('frame'))
->where('index',$this->argument('index'))
->firstOrFail();
} catch (ModelNotFoundException $e) {
$this->error('Page not found to delete: '.$this->argument('frame').$this->argument('index'));
die(1);
}
$o->delete();
$this->info('Page deleted: '.$this->argument('frame').$this->argument('index'));
return self::SUCCESS;
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace App\Console\Commands\BBS;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use App\Models\BBS\{Frame,Mode};
use Illuminate\Support\Arr;
class FrameImport extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bbs:frame:import {frame} {file} '.
'{--index=a : The frame index }'.
'{--access=0 : Is frame accessible }'.
'{--public=0 : Is frame limited to CUG }'.
'{--cost=0 : Frame Cost }'.
'{--mode=Ansi : Frame Emulation Mode }'.
'{--replace : Replace existing frame}'.
'{--type=i : Frame Type}'.
'{--title= : Frame Title}'.
'{--keys= : Key Destinations [0,1,2,3,4,5,6,7,8,9]}'.
'{--trim= : Trim off header (first n chars)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Import frames into the database. The frames should be in binary format.';
/**
* Execute the console command.
*
* @return mixed
* @throws \Exception
*/
public function handle()
{
if (! is_numeric($this->argument('frame')))
throw new \Exception('Frame is not numeric: '.$this->argument('frame'));
if ((strlen($this->option('index')) !== 1) || (! preg_match('/^[a-z]$/',$this->option('index'))))
throw new \Exception('Subframe failed validation');
if (! file_exists($this->argument('file')))
throw new \Exception('File not found: '.$this->argument('file'));
$mo = Mode::where('name',$this->option('mode'))->firstOrFail();
$o = new Frame;
if ($this->option('replace')) {
try {
$o = $o->where('frame',$this->argument('frame'))
->where('index',$this->option('index'))
->where('mode_id',$mo->id)
->orderBy('created_at','DESC')
->firstOrNew();
} catch (ModelNotFoundException $e) {
$this->error('Frame not found to replace: '.$this->argument('frame').$this->option('index'));
exit(1);
}
}
$o->frame = $this->argument('frame');
$o->index = $this->option('index');
$o->mode_id = $mo->id;
$o->access = $this->option('access');
$o->public = $this->option('public');
$o->cost = $this->option('cost');
$o->type = $this->option('type');
$o->title = $this->option('title');
$keys = [];
if ($this->option('keys'))
$keys = explode(',',$this->option('keys'));
foreach (range(0,9) as $key) {
$index = sprintf('r%d',$key);
$o->{$index} = (($x=Arr::get($keys,$key,NULL)) === "null") ? NULL : $x;
}
// We need to escape any back slashes, so they dont get interpretted as hex
$o->content = $this->option('trim')
? substr(file_get_contents($this->argument('file')),$this->option('trim'))
: file_get_contents($this->argument('file'));
// If we have 0x1aSAUCE, we'll discard the sauce.
if ($x = strpos($o->content,chr(0x1a).'SAUCE')) {
$o->content = substr($o->content,0,$x-1).chr(0x0a);
}
$o->save();
$this->info(sprintf('Saved frame: [%s] as [%s] with [%d]',$o->page,$mo->name,$o->id));
}
}

View File

@@ -0,0 +1,128 @@
<?php
/**
* Viewdata/Videotex Server
*
* Inspired by Rob O'Donnell at irrelevant.com
*/
namespace App\Console\Commands\BBS;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Classes\BBS\Server\{Ansitex,Videotex};
use App\Classes\Sock\{SocketException,SocketServer};
use App\Models\Mode;
use App\Models\Setup;
class Start extends Command
{
private const LOGKEY = 'CBS';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bbs:start {--mode=VideoTex : Server Mode Profile}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Start BBS Server';
/**
* Execute the console command.
*
* @return void
* @throws SocketException
*/
public function handle()
{
Log::channel('bbs')->info(sprintf('%s:+ BBS Server Starting (%d)',self::LOGKEY,getmypid()));
$o = Setup::findOrFail(config('app.id'));
$start = collect();
if (TRUE || $o->ansitex_active)
$start->put('ansitex',[
'address'=>$o->ansitex_bind,
'port'=>$o->ansitex_port,
'proto'=>SOCK_STREAM,
'class'=>new Ansitex,
]);
if (TRUE || $o->viewdata_active)
$start->put('videotex',[
'address'=>$o->videotex_bind,
'port'=>$o->videotex_port,
'proto'=>SOCK_STREAM,
'class'=>new Videotex,
]);
$children = collect();
Log::channel('bbs')->debug(sprintf('%s:# Servers [%d]',self::LOGKEY,$start->count()));
if (! $start->count()) {
Log::channel('bbs')->alert(sprintf('%s:! No servers configured to start',self::LOGKEY));
return;
}
//pcntl_signal(SIGCHLD,SIG_IGN);
foreach ($start as $item => $config) {
Log::channel('bbs')->debug(sprintf('%s:- Starting [%s] (%d)',self::LOGKEY,$item,getmypid()));
$pid = pcntl_fork();
if ($pid === -1)
die('could not fork');
// We are the child
if (! $pid) {
Log::channel('bbs')->withContext(['pid'=>getmypid()]);
Log::channel('bbs')->info(sprintf('%s:= Started [%s]',self::LOGKEY,$item));
$server = new SocketServer($config['port'],$config['address'],$config['proto']);
$server->handler = [$config['class'],'onConnect'];
try {
$server->listen();
} catch (SocketException $e) {
if ($e->getMessage() === 'Can\'t accept connections: "Success"')
Log::channel('bbs')->debug(sprintf('%s:! Server Terminated [%s]',self::LOGKEY,$item));
else
Log::channel('bbs')->emergency(sprintf('%s:! Uncaught Message: %s',self::LOGKEY,$e->getMessage()));
}
Log::channel('bbs')->info(sprintf('%s:= Finished: [%s]',self::LOGKEY,$item));
// Child finished we need to get out of this loop.
exit;
}
Log::channel('bbs')->debug(sprintf('%s:- Forked for [%s] (%d)',self::LOGKEY,$item,$pid));
$children->put($pid,$item);
}
// Wait for children to exit
while ($x=$children->count()) {
// Wait for children to finish
$exited = pcntl_wait($status);
if ($exited < 0)
abort(500,sprintf('Something strange for status: [%s] (%d)',pcntl_wifsignaled($status) ? pcntl_wtermsig($status) : 'unknown',$exited));
Log::channel('bbs')->info(sprintf('%s:= Exited: #%d [%s]',self::LOGKEY,$x,$children->pull($exited)));
}
// Done
Log::channel('bbs')->debug(sprintf('%s:= Finished.',self::LOGKEY));
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Classes\Protocol\Binkp;
use App\Classes\Sock\SocketException;
use App\Classes\Sock\SocketServer;
use App\Models\Setup;
class CommBinkpReceive extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'comm:binkp:receive';
/**
* The console command description.
*
* @var string
*/
protected $description = 'BINKP receive';
/**
* Execute the console command.
*
* @return mixed
* @throws SocketException
*/
public function handle()
{
Log::info('Listening for BINKP connections...');
$o = Setup::findOrFail(config('app.id'));
$server = new SocketServer($o->binkp_port,$o->binkp_bind);
$server->handler = [new Binkp($o),'onConnect'];
try {
$server->listen();
} catch (SocketException $e) {
if ($e->getMessage() === 'Can\'t accept connections: "Success"')
Log::debug('Server Terminated');
else
Log::emergency('Uncaught Message: '.$e->getMessage());
}
}
}

View File

@@ -16,9 +16,7 @@ class CommBinkpSend extends Command
*
* @var string
*/
protected $signature = 'comm:binkp:send'
.' {--N|now : Dont queue}'
.' {ftn : FTN to Send to}';
protected $signature = 'comm:binkp:send {ftn : FTN to Send to}';
/**
* The console command description.
@@ -27,12 +25,14 @@ class CommBinkpSend extends Command
*/
protected $description = 'BINKP send';
private const ID = 'BINKP';
/**
* Execute the console command.
*
* @throws \Exception
*/
public function handle()
public function handle(): void
{
$ao = Address::findFTN($this->argument('ftn'));
if (! $ao)
@@ -40,13 +40,8 @@ class CommBinkpSend extends Command
Log::info(sprintf('CBS:- Call BINKP send for %s',$ao->ftn));
$mo = Mailer::where('name','BINKP')->sole();
$mo = Mailer::where('name',self::ID)->singleOrFail();
if ($this->option('now'))
Job::dispatchSync($ao,$mo);
else
Job::dispatch($ao,$mo);
return self::SUCCESS;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use App\Classes\Protocol\EMSI;
use App\Classes\Sock\SocketException;
use App\Classes\Sock\SocketServer;
use App\Models\Setup;
class CommEMSIReceive extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'comm:emsi:receive';
/**
* The console command description.
*
* @var string
*/
protected $description = 'EMSI receive';
/**
* Execute the console command.
*
* @return mixed
* @throws \Exception
*/
public function handle()
{
Log::info('Listening for EMSI connections...');
$o = Setup::findOrFail(config('app.id'));
$server = new SocketServer($o->emsi_port,$o->emsi_bind);
$server->handler = [new EMSI($o),'onConnect'];
try {
$server->listen();
} catch (SocketException $e) {
if ($e->getMessage() === 'Can\'t accept connections: "Success"')
Log::debug('Server Terminated');
else
Log::emergency('Uncaught Message: '.$e->getMessage());
}
}
}

View File

@@ -16,9 +16,7 @@ class CommEMSISend extends Command
*
* @var string
*/
protected $signature = 'comm:emsi:send'
.' {--N|now : Dont queue}'
.' {ftn : FTN to Send to}';
protected $signature = 'comm:emsi:send {ftn : FTN to Send to}';
/**
* The console command description.
@@ -27,12 +25,14 @@ class CommEMSISend extends Command
*/
protected $description = 'EMSI send';
private const ID = 'EMSI';
/**
* Execute the console command.
*
* @throws \Exception
*/
public function handle()
public function handle(): void
{
$ao = Address::findFTN($this->argument('ftn'));
if (! $ao)
@@ -40,13 +40,8 @@ class CommEMSISend extends Command
Log::info(sprintf('CES:- Call EMSI send for %s',$ao->ftn));
$mo = Mailer::where('name','EMSI')->sole();
$mo = Mailer::where('name',self::ID)->singleOrFail();
if ($this->option('now'))
Job::dispatchSync($ao,$mo);
else
Job::dispatch($ao,$mo);
return self::SUCCESS;
}
}

View File

@@ -24,7 +24,7 @@ class AddressCheck extends Command
$this->info(sprintf('Address: %s (%s)',$o->ftn,$o->role_name));
$this->info(sprintf("Children: \n- %s",$o->children()->pluck('ftn4d')->join("\n- ")));
$this->info(sprintf("Downlinks: \n- %s",$o->downlinks()->pluck('ftn4d')->join("\n- ")));
$this->info(sprintf("Downstream: \n- %s",$o->downstream()->pluck('ftn4d')->join("\n- ")));
$this->info(sprintf('Uplink: %s (Parent: %s)',$o->uplink()?->ftn,$o->parent()?->ftn));
$this->info(sprintf('Our Address: %s',our_address($o)?->ftn));
$this->info(sprintf('- Domain Addresses: %s',our_address($o->zone->domain)->pluck('ftn4d')->join(',')));

View File

@@ -1,64 +0,0 @@
<?php
namespace App\Console\Commands\Debug;
use Illuminate\Console\Command;
use App\Models\Address;
class AddressCheckNode extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'debug:address:check:nodes'
.' {ftn? : FTN}'
.' {--N|node : Node Order}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check all addresses we use for nodes';
/**
* Execute the console command.
*
* @return int
* @throws \Exception
*/
public function handle(): int
{
$ao = NULL;
if ($this->argument('ftn')) {
$ao = Address::findFTN($this->argument('ftn'));
if (! $ao) {
$this->error('FTN not found: ' .$this->argument('ftn'));
return self::FAILURE;
}
$this->info('our address:'.our_address($ao)->ftn);
return self::SUCCESS;
}
$this->table(['System','Node','Ours'],
our_nodes($ao ? $ao->domain : NULL)
->sortBy(fn($item)=>$this->option('node')
? sprintf('%s:%s',$item->system->name,$item->domain->name)
: sprintf('%s',$item->domain->name))
->map(fn($item)=>
[
'System'=>$item->system->name,
'Node'=>$item->ftn.' '.($item->echoareas->count() ? '^' : '').($item->fileareas->count() ? '*' : ''),
'Ours'=>our_address($item)?->ftn,
]));
return self::SUCCESS;
}
}

View File

@@ -12,7 +12,8 @@ use App\Models\Dynamic as DynamicModel;
class DynamicItem extends Command
{
protected $signature = 'debug:dynamic:item'
.' {name : Dynamic Item}';
.' {name : Dynamic Item}'
.' {ftn : FTN Address}';
protected $description = 'Generate a dynamic item';
@@ -23,12 +24,12 @@ class DynamicItem extends Command
if (! $do)
throw new \Exception(sprintf('Dynamic Item [%s] doesnt exist?',$this->argument('name')));
$d = new Dynamic($do,$do->address,Send::T_FILE);
$ao = Address::findFTN($this->argument('ftn'));
$d = new Dynamic($do,$ao,Send::T_FILE);
$d->open();
echo $d->read($d->size)."\n";
$this->alert('File sent as:'.$d->nameas);
echo $d->read($d->size);
return self::SUCCESS;
}

View File

@@ -16,7 +16,8 @@ class PacketDump extends Command
protected $signature = 'debug:packet:dump'.
' {type : Type of packet, netmail|echomail }'.
' {ftn : FTN}'.
' {file? : filename}';
' {file? : filename}'.
' {--dump : Dump packet}';
/**
* The console command description.
@@ -49,16 +50,11 @@ class PacketDump extends Command
throw new \Exception('Unknown type: '.$this->argument('type'));
}
if (is_null($pkt)) {
$this->info(sprintf('No packet for [%s] of type [%s]',$this->argument('ftn'),$this->argument('type')));
return self::SUCCESS;
}
if (! $this->argument('file')) {
if ($this->option('dump')) {
$this->info('Item Name:'.$pkt->name);
$this->info('Item Type:'.get_class($pkt));
$this->info('Dump:');
echo hex_dump((string)$pkt);
echo hex_dump($pkt);
} else {
$f = fopen($this->argument('file'),'w+');

View File

@@ -16,7 +16,7 @@ class ZoneCheck extends Command
public function handle(): int
{
$do = Domain::where('name',$this->argument('domain'))->sole();
$do = Domain::where('name',$this->argument('domain'))->singleOrFail();
foreach ($do->zones->sortby('zone_id') as $zo) {
if ($this->option('zone') && ($this->option('zone') != $zo->zone_id))
@@ -25,11 +25,9 @@ class ZoneCheck extends Command
$this->warn('Zone: '.$zo->zone_id);
$this->info(sprintf('- Our address(es): %s',our_address($do)->pluck('ftn4d')->join(',')));
$this->table(['id','region_id','ftn','role','parent','children','downlinks','uplink','send from','system','notes'],
$zo->addresses()->FTN()->active()->with(['system','nodes_hub'])->get()->transform(function($item) {
$this->table(['id','ftn','role','parent','children','downlinks','uplink','send from','region_id','system','notes'],$zo->addresses()->FTNorder()->active()->with(['system'])->dontCache()->get()->transform(function($item) {
return [
'id'=>$item->id,
'region_id'=>$item->region_id,
'ftn'=>$item->ftn4d,
'role'=>$item->role_name,
'parent'=>$item->parent()?->ftn4d,
@@ -37,6 +35,7 @@ class ZoneCheck extends Command
'downlinks'=>$item->downlinks()->count(),
'uplink'=>($x=$item->uplink())?->ftn4d,
'send from'=>$x ? our_address($item->uplink())?->ftn4d : '',
'region_id'=>$item->region_id,
'system'=>$item->system->name,
'notes'=>$item->isRoleOverride() ? 'Role Override' : '',
];

View File

@@ -34,8 +34,7 @@ class EchoareaImport extends Command
*/
public function handle(): int
{
$do = Domain::where('name',strtolower($this->argument('domain')))->single();
return Job::dispatchSync($this->argument('file'),$do,$this->option('prefix') ?: '',$this->option('unlink'));
$do = Domain::where('name',strtolower($this->argument('domain')))->singleOrFail();
return Job::dispatchSync($this->argument('file'),$do,$this->option('prefix'),$this->option('unlink'));
}
}

View File

@@ -34,8 +34,7 @@ class FileareaImport extends Command
*/
public function handle(): int
{
$do = Domain::where('name',strtolower($this->argument('domain')))->sole();
return Job::dispatchSync($this->argument('file'),$do,$this->option('prefix') ?: '',$this->option('unlink'));
$do = Domain::where('name',strtolower($this->argument('domain')))->singleOrFail();
return Job::dispatchSync($this->argument('file'),$do,$this->option('prefix'),$this->option('unlink'));
}
}

View File

@@ -2,10 +2,10 @@
namespace App\Console\Commands\Filefix;
use Carbon\Carbon;
use Illuminate\Console\Command;
use App\Jobs\FilefixRescan;
use App\Models\{Address,Filearea};
use App\Models\{Address,Filearea,File};
class Rescan extends Command
{
@@ -14,13 +14,7 @@ class Rescan extends Command
*
* @var string
*/
protected $signature = 'filefix:rescan'
.' {ftn : FTN Address}'
.' {area : Echoarea Tag}'
.' {days? : Limit to files received days ago}'
.' {--j|queue : Queue the Job}'
.' {--Q|queuename=default : Queue on queue}'
.' {--R|export : Re-export previously sent files}';
protected $signature = 'filefix:rescan {ftn} {area} {file?}';
/**
* The console command description.
@@ -37,9 +31,6 @@ class Rescan extends Command
*/
public function handle(): int
{
if (($this->argument('days')) && (! is_numeric($this->argument('days'))))
throw new \Exception('Days must be numeric: '.$this->argument('days'));
$ao = Address::findFtn($this->argument('ftn'));
if (! $ao)
@@ -49,15 +40,50 @@ class Rescan extends Command
if (! $this->argument('area'))
throw new \Exception('Areaname is required');
$fao = Filearea::where('name',$this->argument('area'))->sole();
$fao = Filearea::where('name',$this->argument('area'))->singleOrFail();
if ($fao->domain_id !== $ao->zone->domain_id)
throw new \Exception(sprintf('File area [%s] is not in domain [%s] for FTN [%s]',$fao->name,$ao->zone->domain->name,$ao->ftn));
if ($this->option('queue'))
FilefixRescan::dispatch($ao,$fao,$this->argument('days'))->onQueue($this->option('queuename'));
else
FilefixRescan::dispatchSync($ao,$fao,$this->argument('days'));
// Check that the user is subscribed
if (! $ao->fileareas->contains($fao->id))
throw new \Exception(sprintf('FTN [%s] is not subscribed to [%s]',$ao->ftn,$fao->name));
// Check that an FTN can read the area
if (! $fao->can_read($ao->security))
throw new \Exception(sprintf('FTN [%s] doesnt have permission to receive [%s]',$ao->ftn,$fao->name));
foreach (File::select('id')
->where('filearea_id',$fao->id)
->when($this->argument('file'),function($query) {
return $query->where('name','=',$this->argument('days'));
})
->orderBy('datetime')
->cursor() as $fo) {
// File hasnt been exported before
if (! $fo->seenby->count()) {
$fo->seenby()->attach($ao->id,['export_at'=>Carbon::now()]);
$this->info(sprintf('Exported [%d] to [%s]',$fo->id,$ao->ftn3d));
} else {
$export = $fo->seenby->where('id',$ao->id)->pop();
// File is pending export
if ($export && $export->pivot->export_at && is_null($export->pivot->sent_at) && is_null($export->pivot->sent_pkt)) {
$this->warn(sprintf('Not exporting [%d] already queued for [%s]',$fo->id,$ao->ftn3d));
// File has been exported
} elseif ($export) {
$fo->seenby()->updateExistingPivot($ao,['export_at'=>Carbon::now(),'sent_at'=>NULL]);
$this->info(sprintf('Re-exported [%d] to [%s]',$fo->id,$ao->ftn3d));
// File has not been exported
} else {
$fo->seenby()->attach($ao,['export_at'=>Carbon::now(),'sent_at'=>NULL]);
$this->info(sprintf('Exported [%d] to [%s]',$fo->id,$ao->ftn3d));
}
}
}
return self::SUCCESS;
}

View File

@@ -45,12 +45,12 @@ class MailList extends Command
'from' => 'FROM',
'to' => 'TO',
'subject' => 'SUBJECT',
],$ao->netmailWaiting()->get()->map(function($item) {
],$ao->netmailWaiting()->map(function($item) {
return [
'id'=>$item->id,
'msgid'=>$item->msgid,
'from'=>sprintf('%s (%s)',$item->from,$item->fftn->ftn3d),
'to'=>sprintf('%s (%s)',$item->to,$item->tftn->ftn3d),
'from'=>$item->from,
'to'=>$item->to,
'subject'=>$item->subject,
];
}));
@@ -63,7 +63,7 @@ class MailList extends Command
'to' => 'TO',
'subject' => 'SUBJECT',
'area' => 'AREA',
],$ao->echomailWaiting()->get()->map(function($item) {
],$ao->echomailWaiting()->map(function($item) {
return [
'id'=>$item->id,
'msgid'=>$item->msgid,

View File

@@ -36,6 +36,7 @@ class NodelistImport extends Command
*/
public function handle():int
{
try {
return Job::dispatchSync(
is_numeric($x=$this->argument('file'))
? File::findOrFail($x)
@@ -46,5 +47,11 @@ class NodelistImport extends Command
$this->option('test'),
$this->option('ignorecrc'),
);
} catch (\Exception $e) {
$this->error($e->getMessage());
return self::FAILURE;
}
}
}

View File

@@ -1,50 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Address;
use Carbon\Carbon;
use Illuminate\Console\Command;
use App\Jobs\NodesNew as Job;
use App\Models\Domain;
class NodesNew extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'nodes:new'
.' {domain : Domain}'
.' {--date= : From a specific date (default 1 since last Saturday)}'
.' {--netmail= : Send a Netmail to FTN}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'List new nodes since last Saturday (or a specific date)';
/**
* Execute the console command.
*/
public function handle(): int
{
$do = Domain::where('name',$this->argument('domain'))->sole();
$ao = NULL;
if ($this->option('netmail')) {
$ao = Address::findFTN($this->option('netmail'));
if (! $ao) {
$this->error('Address not found: '.$this->option('netmail'));
return self::FAILURE;
}
}
return Job::dispatchSync($do,$this->option('date') ? Carbon::parse($this->option('date')) : Carbon::parse('last saturday'),$ao);
}
}

View File

@@ -50,7 +50,7 @@ class PacketInfo extends Command
}
foreach ($f as $packet) {
$pkt = Packet::process($packet,$x=$f->itemName(),$f->itemSize(),$a?->system);
$pkt = Packet::process($packet,$x=$f->itemName(),$f->itemSize(),$a?->zone->domain);
$this->alert(sprintf('File Name: %s',$x));
@@ -68,8 +68,7 @@ class PacketInfo extends Command
echo "\n";
try {
$this->warn(sprintf('- TYPE : %s',get_class($msg)));
$this->warn(sprintf(' - Date : %s (%s)',$msg->date,$msg->date->tz->toOffsetName()));
$this->warn(sprintf('- Date : %s (%s)',$msg->datetime,$msg->datetime->tz->toOffsetName()));
$this->warn(sprintf(' - Errors : %s',$msg->errors->count() ? 'YES' : 'No'));
$this->warn(sprintf(' - Flags : %s',$msg->flags()->keys()->join(', ')));
$this->warn(sprintf(' - Cost : %d',$msg->cost));
@@ -77,7 +76,7 @@ class PacketInfo extends Command
if ($msg instanceof Echomail)
$this->warn(sprintf(' - To : %s',$msg->to));
else
$this->warn(sprintf(' - To : %s (%s)',$msg->to,$msg->tftn?->ftn ?: $msg->set_tftn));
$this->warn(sprintf(' - To : %s (%s)',$msg->to,$msg->tftn->ftn));
$this->warn(sprintf(' - Subject: %s',$msg->subject));
if ($msg instanceof Echomail)
$this->warn(sprintf(' - Area : %s',$msg->echoarea->name));
@@ -98,7 +97,7 @@ class PacketInfo extends Command
}
foreach ($pkt->errors as $msg) {
$this->error(sprintf('- Date: %s',$msg->datetime));
$this->error(sprintf('- Date: %s',$msg->date));
$this->error(sprintf(' - FLAGS: %s',$msg->flags()->filter()->keys()->join(', ')));
$this->error(sprintf(' - From: %s (%s)',$msg->from,$msg->fftn));
$this->error(sprintf(' - To: %s (%s)',$msg->to,$msg->tftn));

View File

@@ -34,8 +34,6 @@ use App\Models\Address;
* - To areafix (processed)
* - To ping (respond)
* - With trace turned on (respond)
*
* @todo Enable force processing packets when the password is wrong
*/
class PacketProcess extends Command
{
@@ -80,7 +78,7 @@ class PacketProcess extends Command
return self::FAILURE;
}
Job::dispatchSync($rel_name,$ao->system,$this->option('dontqueue'));
Job::dispatchSync($rel_name,$ao->zone->domain,$this->option('dontqueue'));
return self::SUCCESS;
}

View File

@@ -3,12 +3,11 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use App\Classes\Protocol\{Binkp,DNS,EMSI};
use App\Classes\Sock\Exception\SocketException;
use App\Classes\Sock\SocketServer;
use App\Classes\Sock\{SocketException,SocketServer};
use App\Models\Setup;
class ServerStart extends Command
{
@@ -37,11 +36,7 @@ class ServerStart extends Command
public function handle(): int
{
Log::info(sprintf('%s:+ Server Starting (%d)',self::LOGKEY,getmypid()));
if (! our_address()->count())
throw new \Exception('We dont have any ACTIVE FTN addresses assigned');
$o = Config::get('setup');
$o = Setup::findOrFail(config('app.id'));
$start = collect();
@@ -66,7 +61,7 @@ class ServerStart extends Command
'address'=>$o->dns_bind,
'port'=>$o->dns_port,
'proto'=>SOCK_DGRAM,
'class'=>new DNS($o),
'class'=>new DNS(),
]);
$children = collect();

Some files were not shown because too many files have changed in this diff Show More