Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
2dd7a6ebd3 | |||
c9688ef373 |
65
.env.example
65
.env.example
@@ -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=
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
}
|
@@ -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
101
app/Classes/BBS/Control.php
Normal 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;
|
||||
}
|
||||
}
|
198
app/Classes/BBS/Control/EditFrame.php
Normal file
198
app/Classes/BBS/Control/EditFrame.php
Normal 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__);
|
||||
}
|
||||
}
|
158
app/Classes/BBS/Control/Register.php
Normal file
158
app/Classes/BBS/Control/Register.php
Normal 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;
|
||||
}
|
||||
}
|
199
app/Classes/BBS/Control/Telnet.php
Normal file
199
app/Classes/BBS/Control/Telnet.php
Normal 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 '';
|
||||
}
|
||||
}
|
56
app/Classes/BBS/Control/Test.php
Normal file
56
app/Classes/BBS/Control/Test.php
Normal 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;
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\BBS\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ActionMissingInputsException extends Exception
|
||||
{
|
||||
}
|
9
app/Classes/BBS/Exceptions/InvalidPasswordException.php
Normal file
9
app/Classes/BBS/Exceptions/InvalidPasswordException.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\BBS\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class InvalidPasswordException extends Exception
|
||||
{
|
||||
}
|
9
app/Classes/BBS/Exceptions/NoRouteException.php
Normal file
9
app/Classes/BBS/Exceptions/NoRouteException.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\BBS\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class NoRouteException extends Exception
|
||||
{
|
||||
}
|
9
app/Classes/BBS/Exceptions/ParentNotFoundException.php
Normal file
9
app/Classes/BBS/Exceptions/ParentNotFoundException.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\BBS\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ParentNotFoundException extends Exception
|
||||
{
|
||||
}
|
73
app/Classes/BBS/Frame/Action.php
Normal file
73
app/Classes/BBS/Frame/Action.php
Normal 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(',')));
|
||||
}
|
||||
}
|
50
app/Classes/BBS/Frame/Action/Login.php
Normal file
50
app/Classes/BBS/Frame/Action/Login.php
Normal 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;
|
||||
}
|
||||
}
|
112
app/Classes/BBS/Frame/Action/Register.php
Normal file
112
app/Classes/BBS/Frame/Action/Register.php
Normal 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;
|
||||
}
|
||||
}
|
290
app/Classes/BBS/Frame/Char.php
Normal file
290
app/Classes/BBS/Frame/Char.php
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
110
app/Classes/BBS/Frame/Field.php
Normal file
110
app/Classes/BBS/Frame/Field.php
Normal 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
628
app/Classes/BBS/Page.php
Normal 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;
|
||||
}
|
||||
}
|
433
app/Classes/BBS/Page/Ansi.php
Normal file
433
app/Classes/BBS/Page/Ansi.php
Normal 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;
|
||||
}
|
||||
}
|
370
app/Classes/BBS/Page/Viewdata.php
Normal file
370
app/Classes/BBS/Page/Viewdata.php
Normal 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
1231
app/Classes/BBS/Server.php
Normal file
File diff suppressed because it is too large
Load Diff
87
app/Classes/BBS/Server/Ansitex.php
Normal file
87
app/Classes/BBS/Server/Ansitex.php
Normal 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;
|
||||
}
|
||||
}
|
91
app/Classes/BBS/Server/Videotex.php
Normal file
91
app/Classes/BBS/Server/Videotex.php
Normal 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
365
app/Classes/BBS/Window.php
Normal 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];
|
||||
}
|
||||
}
|
@@ -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');
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
@@ -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,159 +682,136 @@ 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);
|
||||
while (substr($message,$ptr_start,1) === "\x01") {
|
||||
$ptr_end = strpos($message,"\r",$ptr_start);
|
||||
|
||||
$m = [];
|
||||
$kludge = ($x=substr($message,$ptr_start+1,$ptr_end-$ptr_start-1));
|
||||
preg_match('/^([^\s]+:?)+\s+(.*)$/',$kludge,$m);
|
||||
|
||||
$ptr_start = $ptr_end+1;
|
||||
|
||||
if (! $m) {
|
||||
Log::alert(sprintf('%s:! Invalid Kluge Line [%s]',self::LOGKEY,$x));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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';
|
||||
|
||||
$this->{self::kludges[$m[1]]} = $m[2];
|
||||
|
||||
} else
|
||||
$o->kludges = [$m[1],$m[2]];
|
||||
}
|
||||
|
||||
// Next our message content ends with '\r * Origin: ... \r' or <soh>...
|
||||
// FTS-0004.001
|
||||
if ($ptr_end=strrpos($message,"\r * Origin: ",$ptr_start)) {
|
||||
// Find the <cr>
|
||||
$ptr_end = strpos($message,"\r",$ptr_end+1);
|
||||
|
||||
// If there is no ptr_end, then this is not an origin
|
||||
if (! $ptr_end)
|
||||
throw new InvalidPacketException('Couldnt find the end of the origin');
|
||||
|
||||
} elseif (! $ptr_end=strpos($message,"\r\x01",$ptr_start)) {
|
||||
$ptr_end = strlen($message);
|
||||
}
|
||||
|
||||
$remaining = substr($message,$ptr_end+1);
|
||||
|
||||
// At this point, the remaining part of the message should start with \x01, PATH or SEEN-BY
|
||||
if ((substr($remaining,0,9) !== 'SEEN-BY: ') && (substr($remaining,0,5) !== 'PATH:') && ($x=strpos($remaining,"\x01")) !== 0) {
|
||||
if ($x)
|
||||
$ptr_end += $x;
|
||||
else
|
||||
$ptr_end += strlen($remaining);
|
||||
}
|
||||
|
||||
// Process the message content
|
||||
if ($content=substr($message,$ptr_start,$ptr_end-$ptr_start)) {
|
||||
$o->msg_src = $content;
|
||||
$o->msg_crc = md5($content);
|
||||
$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);
|
||||
|
||||
$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
|
||||
if (! $ptr_content_end) {
|
||||
$o->set_tagline = substr($content,$ptr_content_start);
|
||||
$ptr_content_start = strlen($content);
|
||||
|
||||
} else {
|
||||
$o->set_tagline = substr($content,$ptr_content_start,$ptr_content_end-$ptr_content_start);
|
||||
$ptr_content_start = $ptr_content_end;
|
||||
}
|
||||
}
|
||||
|
||||
// See if we have a tearline
|
||||
if ($ptr_content_end=strrpos($content,"\r--- ",$ptr_content_start)) {
|
||||
if (! $ptr_content_start)
|
||||
$o->msg = substr($content,$ptr_content_start,$ptr_content_end+1);
|
||||
|
||||
$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
|
||||
if (! $ptr_content_end) {
|
||||
$o->set_tearline = substr($content,$ptr_content_start);
|
||||
$ptr_content_start = strlen($content);
|
||||
|
||||
} else {
|
||||
$o->set_tearline = substr($content,$ptr_content_start,$ptr_content_end-$ptr_content_start);
|
||||
$ptr_content_start = $ptr_content_end;
|
||||
}
|
||||
}
|
||||
|
||||
// See if we have an origin
|
||||
if ($ptr_content_end=strrpos($content,"\r * Origin: ",$ptr_content_start)) {
|
||||
if (! $ptr_content_start)
|
||||
$o->msg = substr($content,$ptr_content_start,$ptr_content_end);
|
||||
|
||||
$ptr_content_start = $ptr_content_end+12;
|
||||
|
||||
$ptr_content_end = strpos($content,"\r",$ptr_content_start);
|
||||
|
||||
// If there is no terminating "\r", then that's it
|
||||
if (! $ptr_content_end) {
|
||||
$o->set_origin = substr($content,$ptr_content_start);
|
||||
$ptr_content_start = strlen($content);
|
||||
|
||||
} else {
|
||||
$o->set_origin = substr($content,$ptr_content_start,$ptr_content_end-$ptr_content_start);
|
||||
$ptr_content_start = $ptr_content_end+1;
|
||||
}
|
||||
}
|
||||
|
||||
// If there wasnt any tagline/tearline/origin, then the whole content is the message
|
||||
if (! $ptr_content_start) {
|
||||
$o->msg = $content;
|
||||
$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);
|
||||
}
|
||||
}
|
||||
$m = [];
|
||||
$kludge = ($x=substr($message,$ptr_start+1,$ptr_end-$ptr_start-1));
|
||||
preg_match('/^([^\s]+:?)+\s+(.*)$/',$kludge,$m);
|
||||
|
||||
$ptr_start = $ptr_end+1;
|
||||
|
||||
// Finally work out control kludges
|
||||
foreach (collect(explode("\r",substr($message,$ptr_start)))->filter() as $line) {
|
||||
// If the line starts with <soh> ignore it
|
||||
if (substr($line,0,1) === "\x01")
|
||||
$line = ltrim($line,"\x01");
|
||||
|
||||
$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]];
|
||||
if (! $m) {
|
||||
Log::alert(sprintf('%s:! Invalid Kluge Line [%s]',self::LOGKEY,$x));
|
||||
continue;
|
||||
}
|
||||
|
||||
} 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()]);
|
||||
// Catch any kludges we need to process here
|
||||
if (array_key_exists($m[1],self::kludges))
|
||||
$this->{self::kludges[$m[1]]} = $m[2];
|
||||
else
|
||||
$o->kludges = [$m[1],$m[2]];
|
||||
}
|
||||
|
||||
throw new InvalidPacketException('Error parsing message');
|
||||
// Next our message content ends with '\r * Origin: ... \r' or <soh>...
|
||||
// FTS-0004.001
|
||||
if ($ptr_end=strrpos($message,"\r * Origin: ",$ptr_start)) {
|
||||
// Find the <cr>
|
||||
$ptr_end = strpos($message,"\r",$ptr_end+1);
|
||||
|
||||
// If there is no ptr_end, then this is not an origin
|
||||
if (! $ptr_end)
|
||||
throw new InvalidPacketException('Couldnt find the end of the origin');
|
||||
|
||||
} elseif (! $ptr_end=strpos($message,"\r\x01",$ptr_start)) {
|
||||
throw new InvalidPacketException('Couldnt parse the end of the content');
|
||||
}
|
||||
|
||||
$remaining = substr($message,$ptr_end+1);
|
||||
|
||||
// At this point, the remaining part of the message should start with \x01, PATH or SEEN-BY
|
||||
if ((substr($remaining,0,9) !== 'SEEN-BY: ') && (substr($remaining,0,5) !== 'PATH:') && ($x=strpos($remaining,"\x01")) !== 0) {
|
||||
if ($x)
|
||||
$ptr_end += $x;
|
||||
else
|
||||
$ptr_end += strlen($remaining);
|
||||
}
|
||||
|
||||
// Process the message content
|
||||
if ($content=substr($message,$ptr_start,$ptr_end-$ptr_start)) {
|
||||
$o->msg_src = $content;
|
||||
$o->msg_crc = md5($content);
|
||||
$ptr_content_start = 0;
|
||||
|
||||
// See if we have a tagline
|
||||
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_end = strpos($content,"\r",$ptr_content_start);
|
||||
|
||||
// If there is no terminating "\r", then that's it
|
||||
if (! $ptr_content_end) {
|
||||
$o->set_tagline = substr($content,$ptr_content_start);
|
||||
$ptr_content_start = strlen($content);
|
||||
|
||||
} else {
|
||||
$o->set_tagline = substr($content,$ptr_content_start,$ptr_content_end-$ptr_content_start);
|
||||
$ptr_content_start = $ptr_content_end;
|
||||
}
|
||||
}
|
||||
|
||||
// See if we have a tearline
|
||||
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);
|
||||
$ptr_content_start = $ptr_content_end+6;
|
||||
|
||||
$ptr_content_end = strpos($content,"\r",$ptr_content_start);
|
||||
|
||||
// If there is no terminating "\r", then that's it
|
||||
if (! $ptr_content_end) {
|
||||
$o->set_tearline = substr($content,$ptr_content_start);
|
||||
$ptr_content_start = strlen($content);
|
||||
|
||||
} else {
|
||||
$o->set_tearline = substr($content,$ptr_content_start,$ptr_content_end-$ptr_content_start);
|
||||
$ptr_content_start = $ptr_content_end;
|
||||
}
|
||||
}
|
||||
|
||||
// See if we have an origin
|
||||
if ($ptr_content_end=strrpos($content,"\r * Origin: ",$ptr_content_start)) {
|
||||
if (! $ptr_content_start)
|
||||
$o->msg = substr($content,$ptr_content_start,$ptr_content_end);
|
||||
|
||||
$ptr_content_start = $ptr_content_end+12;
|
||||
|
||||
$ptr_content_end = strpos($content,"\r",$ptr_content_start);
|
||||
|
||||
// If there is no terminating "\r", then that's it
|
||||
if (! $ptr_content_end) {
|
||||
$o->set_origin = substr($content,$ptr_content_start);
|
||||
$ptr_content_start = strlen($content);
|
||||
|
||||
} else {
|
||||
$o->set_origin = substr($content,$ptr_content_start,$ptr_content_end-$ptr_content_start);
|
||||
$ptr_content_start = $ptr_content_end+1;
|
||||
}
|
||||
}
|
||||
|
||||
// If there wasnt any tagline/tearline/origin, then the whole content is the message
|
||||
if (! $ptr_content_start) {
|
||||
$o->msg = $content;
|
||||
$ptr_content_start = $ptr_end-$ptr_start;
|
||||
}
|
||||
|
||||
// Quick validation that we are done
|
||||
if ($ptr_content_start !== strlen($content))
|
||||
throw new InvalidPacketException('There is more data in the message content?');
|
||||
}
|
||||
|
||||
$ptr_start = $ptr_end+1;
|
||||
|
||||
// Finally work out control kludges
|
||||
foreach (collect(explode("\r",substr($message,$ptr_start)))->filter() as $line) {
|
||||
// If the line starts with <soh> ignore it
|
||||
if (substr($line,0,1) === "\x01")
|
||||
$line = ltrim($line,"\x01");
|
||||
|
||||
$m = [];
|
||||
preg_match('/^([^\s]+:?)+\s+(.*)$/',$line,$m);
|
||||
$o->kludges = [$m[1],$m[2]];
|
||||
}
|
||||
|
||||
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)
|
||||
|
@@ -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,10 +51,9 @@ 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)
|
||||
protected $pass_p = NULL; // Overwrite the packet password (when packing messages)
|
||||
|
||||
/* ABSTRACT */
|
||||
|
||||
@@ -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,41 +137,26 @@ 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));
|
||||
}
|
||||
$o->zone = Zone::where('zone_id',$o->fz)
|
||||
->where('default',TRUE)
|
||||
->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 = '';
|
||||
$leader = Message::header_len()+strlen(self::PACKED_MSG_LEAD);
|
||||
@@ -189,19 +171,18 @@ 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;
|
||||
|
||||
// If we have more to read
|
||||
// If we have more to read
|
||||
} elseif ($read_ptr < $size) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
@@ -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,39 +62,44 @@ 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();
|
||||
|
||||
return pack(collect(self::HEADER)->pluck(1)->join(''),
|
||||
$this->fftn_p->node_id, // Orig Node
|
||||
$this->tftn_p->node_id, // Dest Node
|
||||
$oldest->datetime->format('Y'), // Year
|
||||
$oldest->datetime->format('m')-1, // Month
|
||||
$oldest->datetime->format('d'), // Day
|
||||
$oldest->datetime->format('H'), // Hour
|
||||
$oldest->datetime->format('i'), // Minute
|
||||
$oldest->datetime->format('s'), // Second
|
||||
0, // Baud
|
||||
2, // Packet Version (should be 2)
|
||||
$this->fftn_p->host_id, // Orig Net
|
||||
$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->fftn_p->zone->zone_id, // Orig Zone
|
||||
$this->tftn_p->zone->zone_id, // Dest Zone
|
||||
'', // Reserved
|
||||
static::VERS, // fsc-0039.004 (copy of 0x2c)
|
||||
((Setup::PRODUCT_ID >> 8) & 0xff), // Product Code Hi
|
||||
Setup::PRODUCT_VERSION_MIN, // Product Version Minor
|
||||
static::VERS, // Capability Word
|
||||
$this->fftn_p->zone->zone_id, // Orig Zone
|
||||
$this->tftn_p->zone->zone_id, // Dest Zone
|
||||
$this->fftn_p->point_id, // Orig Point
|
||||
$this->tftn_p->point_id, // Dest Point
|
||||
strtoupper(hexstr(Setup::PRODUCT_ID)), // ProdData
|
||||
);
|
||||
try {
|
||||
return pack(collect(self::HEADER)->pluck(1)->join(''),
|
||||
$this->fftn_p->node_id, // Orig Node
|
||||
$this->tftn_p->node_id, // Dest Node
|
||||
$oldest->datetime->format('Y'), // Year
|
||||
$oldest->datetime->format('m')-1, // Month
|
||||
$oldest->datetime->format('d'), // Day
|
||||
$oldest->datetime->format('H'), // Hour
|
||||
$oldest->datetime->format('i'), // Minute
|
||||
$oldest->datetime->format('s'), // Second
|
||||
0, // Baud
|
||||
2, // Packet Version (should be 2)
|
||||
$this->fftn_p->host_id, // Orig Net
|
||||
$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->session('pktpass'), // Packet Password
|
||||
$this->fftn_p->zone->zone_id, // Orig Zone
|
||||
$this->tftn_p->zone->zone_id, // Dest Zone
|
||||
'', // Reserved
|
||||
static::VERS, // fsc-0039.004 (copy of 0x2c)
|
||||
((Setup::PRODUCT_ID >> 8) & 0xff), // Product Code Hi
|
||||
Setup::PRODUCT_VERSION_MIN, // Product Version Minor
|
||||
static::VERS, // Capability Word
|
||||
$this->fftn_p->zone->zone_id, // Orig Zone
|
||||
$this->tftn_p->zone->zone_id, // Dest Zone
|
||||
$this->fftn_p->point_id, // Orig Point
|
||||
$this->tftn_p->point_id, // Dest Point
|
||||
strtoupper(hexstr(Setup::PRODUCT_ID)), // ProdData
|
||||
);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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,27 +54,32 @@ final class FSC45 extends Packet
|
||||
/**
|
||||
* Create our message packet header
|
||||
*/
|
||||
protected function header(Collection $msgs): string
|
||||
protected function header(): string
|
||||
{
|
||||
return pack(collect(self::HEADER)->pluck(1)->join(''),
|
||||
$this->fftn_p->node_id, // Orig Node
|
||||
$this->tftn_p->node_id, // Dest Node
|
||||
$this->fftn_p->point_id, // Orig Point
|
||||
$this->tftn_p->point_id, // Dest Point
|
||||
'', // Reserved
|
||||
2, // Sub Version (should be 2)
|
||||
2, // Packet Version (should be 2)
|
||||
$this->fftn_p->host_id, // Orig Net
|
||||
$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->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
|
||||
);
|
||||
try {
|
||||
return pack(collect(self::HEADER)->pluck(1)->join(''),
|
||||
$this->fftn_p->node_id, // Orig Node
|
||||
$this->tftn_p->node_id, // Dest Node
|
||||
$this->fftn_p->point_id, // Orig Point
|
||||
$this->tftn_p->point_id, // Dest Point
|
||||
'', // Reserved
|
||||
2, // Sub Version (should be 2)
|
||||
2, // Packet Version (should be 2)
|
||||
$this->fftn_p->host_id, // Orig Net
|
||||
$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->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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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,39 +62,44 @@ 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();
|
||||
|
||||
return pack(collect(self::HEADER)->pluck(1)->join(''),
|
||||
$this->fftn_p->node_id, // Orig Node
|
||||
$this->tftn_p->node_id, // Dest Node
|
||||
$oldest->datetime->format('Y'), // Year
|
||||
$oldest->datetime->format('m')-1, // Month
|
||||
$oldest->datetime->format('d'), // Day
|
||||
$oldest->datetime->format('H'), // Hour
|
||||
$oldest->datetime->format('i'), // Minute
|
||||
$oldest->datetime->format('s'), // Second
|
||||
0, // Baud
|
||||
2, // Packet Version (should be 2)
|
||||
$this->fftn_p->point_id ? 0xffff : $this->fftn_p->host_id, // Orig Net (0xFFFF when OrigPoint != 0)
|
||||
$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->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
|
||||
static::VERS, // fsc-0039.004 (copy of 0x2c)
|
||||
((Setup::PRODUCT_ID >> 8) & 0xff), // Product Code Hi
|
||||
Setup::PRODUCT_VERSION_MIN, // Product Version Minor
|
||||
static::VERS, // Capability Word
|
||||
$this->fftn_p->zone->zone_id, // Orig Zone
|
||||
$this->tftn_p->zone->zone_id, // Dest Zone
|
||||
$this->fftn_p->point_id, // Orig Point
|
||||
$this->tftn_p->point_id, // Dest Point
|
||||
strtoupper(hexstr(Setup::PRODUCT_ID)), // ProdData
|
||||
);
|
||||
try {
|
||||
return pack(collect(self::HEADER)->pluck(1)->join(''),
|
||||
$this->fftn_p->node_id, // Orig Node
|
||||
$this->tftn_p->node_id, // Dest Node
|
||||
$oldest->datetime->format('Y'), // Year
|
||||
$oldest->datetime->format('m')-1, // Month
|
||||
$oldest->datetime->format('d'), // Day
|
||||
$oldest->datetime->format('H'), // Hour
|
||||
$oldest->datetime->format('i'), // Minute
|
||||
$oldest->datetime->format('s'), // Second
|
||||
0, // Baud
|
||||
2, // Packet Version (should be 2)
|
||||
$this->fftn_p->point_id ? 0xffff : $this->fftn_p->host_id, // Orig Net (0xFFFF when OrigPoint != 0)
|
||||
$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->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
|
||||
static::VERS, // fsc-0039.004 (copy of 0x2c)
|
||||
((Setup::PRODUCT_ID >> 8) & 0xff), // Product Code Hi
|
||||
Setup::PRODUCT_VERSION_MIN, // Product Version Minor
|
||||
static::VERS, // Capability Word
|
||||
$this->fftn_p->zone->zone_id, // Orig Zone
|
||||
$this->tftn_p->zone->zone_id, // Dest Zone
|
||||
$this->fftn_p->point_id, // Orig Point
|
||||
$this->tftn_p->point_id, // Dest Point
|
||||
strtoupper(hexstr(Setup::PRODUCT_ID)), // ProdData
|
||||
);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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,30 +55,35 @@ 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();
|
||||
|
||||
return pack(collect(self::HEADER)->pluck(1)->join(''),
|
||||
$this->fftn_p->node_id, // Orig Node
|
||||
$this->tftn_p->node_id, // Dest Node
|
||||
$oldest->datetime->format('Y'), // Year
|
||||
$oldest->datetime->format('m')-1, // Month
|
||||
$oldest->datetime->format('d'), // Day
|
||||
$oldest->datetime->format('H'), // Hour
|
||||
$oldest->datetime->format('i'), // Minute
|
||||
$oldest->datetime->format('s'), // Second
|
||||
0, // Baud
|
||||
2, // Packet Version (should be 2)
|
||||
$this->fftn_p->host_id, // Orig Net
|
||||
$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->fftn_p->zone->zone_id, // Orig Zone
|
||||
$this->tftn_p->zone->zone_id, // Dest Zone
|
||||
'', // Reserved
|
||||
);
|
||||
try {
|
||||
return pack(collect(self::HEADER)->pluck(1)->join(''),
|
||||
$this->fftn_p->node_id, // Orig Node
|
||||
$this->tftn_p->node_id, // Dest Node
|
||||
$oldest->datetime->format('Y'), // Year
|
||||
$oldest->datetime->format('m')-1, // Month
|
||||
$oldest->datetime->format('d'), // Day
|
||||
$oldest->datetime->format('H'), // Hour
|
||||
$oldest->datetime->format('i'), // Minute
|
||||
$oldest->datetime->format('s'), // Second
|
||||
0, // Baud
|
||||
2, // Packet Version (should be 2)
|
||||
$this->fftn_p->host_id, // Orig Net
|
||||
$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->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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -11,7 +11,7 @@ abstract class Process
|
||||
{
|
||||
public static function canProcess(Echoarea $eao): bool
|
||||
{
|
||||
return (bool)$eao->automsgs;
|
||||
return $eao->automsgs ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
|
37
app/Classes/FTN/Process/Netmail/Areafix.php
Normal file
37
app/Classes/FTN/Process/Netmail/Areafix.php
Normal 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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
||||
|
@@ -22,28 +22,24 @@ class File extends FileBase implements \Iterator
|
||||
{
|
||||
parent::__construct($path,$checkPath);
|
||||
|
||||
if ($this->getExtension() === 'pkt')
|
||||
$this->canHandle = TRUE;
|
||||
switch($x=$this->guessExtension()) {
|
||||
case 'zip':
|
||||
$this->canHandle = TRUE;
|
||||
$this->isArchive = TRUE;
|
||||
$this->z = new \ZipArchive;
|
||||
$this->z->open($this->getRealPath());
|
||||
break;
|
||||
|
||||
else
|
||||
switch ($x=$this->guessExtension()) {
|
||||
case 'zip':
|
||||
case NULL:
|
||||
case 'bin':
|
||||
if ($this->isPacket() || ($path instanceof UploadedFile && (strcasecmp($path->getClientOriginalExtension(),'pkt') === 0))) {
|
||||
$this->canHandle = TRUE;
|
||||
$this->isArchive = TRUE;
|
||||
$this->z = new \ZipArchive;
|
||||
$this->z->open($this->getRealPath());
|
||||
break;
|
||||
}
|
||||
|
||||
case NULL:
|
||||
case 'bin':
|
||||
if ($this->isPacket() || ($path instanceof UploadedFile && (strcasecmp($path->getClientOriginalExtension(),'pkt') === 0))) {
|
||||
$this->canHandle = TRUE;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
Log::alert(sprintf('%s:? Unknown file received: %s (%s) [%s]',self::LOGKEY,$x,$this->getExtension(),$path instanceof UploadedFile ? $path->getClientOriginalExtension() : '-'));
|
||||
}
|
||||
default:
|
||||
Log::alert(sprintf('%s:? Unknown file received: %s (%s) [%s]',self::LOGKEY,$x,$this->getExtension(),$path instanceof UploadedFile ? $path->getClientOriginalExtension() : '-'));
|
||||
}
|
||||
}
|
||||
|
||||
/* ITERATOR */
|
||||
@@ -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
|
||||
*
|
||||
|
@@ -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]
|
||||
|
@@ -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,21 +84,22 @@ 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);
|
||||
|
||||
if (! $this->fd) {
|
||||
Log::error(sprintf('%s:! Unable to open file [%s] for reading',self::LOGKEY,$this->full_name));
|
||||
} else {
|
||||
$this->fd = fopen($this->full_name,'rb');
|
||||
|
||||
return FALSE;
|
||||
if (! $this->fd) {
|
||||
Log::error(sprintf('%s:! Unable to open file [%s] for reading',self::LOGKEY,$this->full_name));
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
Log::info(sprintf('%s:= File [%s] opened with size [%d]',self::LOGKEY,$this->full_name,$this->size));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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()));
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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],
|
||||
],
|
||||
];
|
||||
}
|
@@ -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
|
||||
|
@@ -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) !== ' ')
|
||||
|
@@ -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.
|
||||
@@ -97,17 +97,17 @@ abstract class Protocol
|
||||
|
||||
// File transfer status
|
||||
|
||||
public const FOP_OK = 0;
|
||||
public const FOP_CONT = 1;
|
||||
public const FOP_SKIP = 2;
|
||||
public const FOP_ERROR = 3;
|
||||
public const FOP_OK = 0;
|
||||
public const FOP_CONT = 1;
|
||||
public const FOP_SKIP = 2;
|
||||
public const FOP_ERROR = 3;
|
||||
public const FOP_SUSPEND = 4;
|
||||
public const FOP_GOT = 5;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@@ -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"]);
|
||||
}
|
||||
}
|
@@ -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));
|
||||
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());
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
$ch = $this->client->read_ch(max( 1,min($this->client->timer_rest($t1),$this->client->timer_rest($t2))));
|
||||
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);
|
||||
|
@@ -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));
|
||||
|
@@ -1,6 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Sock\Exception;
|
||||
|
||||
final class HAproxyException extends \Exception {
|
||||
}
|
@@ -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,163 +54,133 @@ 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;
|
||||
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');
|
||||
}
|
||||
|
||||
elseif (($x === "\x0d\x0a\x0d\x0a\x00\x0d") && ($this->read(5,6) === "\x0aQUIT\x0a"))
|
||||
$vers = 2;
|
||||
// Version/Command
|
||||
$vc = $this->read_ch(5);
|
||||
|
||||
else
|
||||
throw new HAproxyException('Failed to initialise HAPROXY connection');
|
||||
if (($x=($vc>>4)&0x7) !== 2) {
|
||||
Log::error(sprintf('%s:! HAPROXY version [%d] is not handled',self::LOGKEY,$x));
|
||||
|
||||
switch ($vers) {
|
||||
throw new SocketException(SocketException::CANT_CONNECT,'Unknown HAPROXY version');
|
||||
}
|
||||
|
||||
switch ($x=($vc&0x7)) {
|
||||
// HAPROXY internal
|
||||
case 0:
|
||||
Log::debug(sprintf('%s:! HAPROXY internal health-check',self::LOGKEY));
|
||||
throw new SocketException(SocketException::CANT_CONNECT,'Healthcheck');
|
||||
|
||||
// PROXY connection
|
||||
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));
|
||||
}
|
||||
|
||||
$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));
|
||||
|
||||
switch ($x=($vc&0x7)) {
|
||||
// HAPROXY internal
|
||||
case 0:
|
||||
throw new HAproxyException('HAPROXY internal health-check');
|
||||
|
||||
// PROXY connection
|
||||
case 1:
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new HAproxyException(sprintf('HAPROXY command [%d] is not handled',$x));
|
||||
}
|
||||
|
||||
// Protocol/Address Family
|
||||
$pa = $this->read_ch(5);
|
||||
|
||||
switch ($x=($pa>>4)&0x7) {
|
||||
case 1: // AF_INET
|
||||
$p = 4;
|
||||
break;
|
||||
|
||||
case 2: // AF_INET6
|
||||
$p = 6;
|
||||
break;
|
||||
|
||||
|
||||
}
|
||||
|
||||
switch ($x=($pa&0x7)) {
|
||||
case 1: // STREAM
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new HAproxyException(sprintf('HAPROXY address family [%d] is not handled',$x));
|
||||
}
|
||||
|
||||
$len = Arr::get(unpack('n',$this->read(5,2)),1);
|
||||
|
||||
// IPv4
|
||||
if (($p === 4) && ($len === 12)) {
|
||||
$src = inet_ntop($this->read(5,4));
|
||||
$dst = inet_ntop($this->read(5,4));
|
||||
|
||||
} elseif (($p === 6) && ($len === 36)) {
|
||||
$src = inet_ntop($this->read(5,16));
|
||||
$dst = inet_ntop($this->read(5,16));
|
||||
|
||||
} else {
|
||||
throw new HAproxyException(sprintf('HAPROXY address len [%d:%d] is not handled',$p,$len));
|
||||
}
|
||||
|
||||
$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');
|
||||
Log::error(sprintf('%s:! HAPROXY command [%d] is not handled',self::LOGKEY,$x));
|
||||
|
||||
throw new SocketException(SocketException::CANT_CONNECT,'Unknown HAPROXY command');
|
||||
}
|
||||
|
||||
$this->address_remote = $src;
|
||||
// Protocol/Address Family
|
||||
$pa = $this->read_ch(5);
|
||||
$p = NULL;
|
||||
|
||||
Log::debug(sprintf('%s:- HAPROXY src [%s:%d] dst [%s:%d]',
|
||||
switch ($x=($pa>>4)&0x7) {
|
||||
case 1: // AF_INET
|
||||
$p = 4;
|
||||
break;
|
||||
|
||||
case 2: // AF_INET6
|
||||
$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)) {
|
||||
case 1: // STREAM
|
||||
break;
|
||||
|
||||
default:
|
||||
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);
|
||||
|
||||
// IPv4
|
||||
if (($p === 4) && ($len === 12)) {
|
||||
$src = inet_ntop($this->read(5,4));
|
||||
$dst = inet_ntop($this->read(5,4));
|
||||
|
||||
} elseif (($p === 6) && ($len === 36)) {
|
||||
$src = inet_ntop($this->read(5,16));
|
||||
$dst = inet_ntop($this->read(5,16));
|
||||
|
||||
} else {
|
||||
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 = unpack('n',$this->read(5,2));
|
||||
|
||||
$this->address_remote = $src;
|
||||
$this->port_remote = Arr::get($src_port,1);
|
||||
|
||||
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));
|
||||
$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;
|
||||
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));
|
||||
|
@@ -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',
|
||||
];
|
@@ -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);
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
37
app/Console/Commands/BBS/ANSIDecode.php
Normal file
37
app/Console/Commands/BBS/ANSIDecode.php
Normal 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;
|
||||
}
|
||||
}
|
42
app/Console/Commands/BBS/ANSIEncode.php
Normal file
42
app/Console/Commands/BBS/ANSIEncode.php
Normal 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;
|
||||
}
|
||||
}
|
53
app/Console/Commands/BBS/FrameDelete.php
Normal file
53
app/Console/Commands/BBS/FrameDelete.php
Normal 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;
|
||||
}
|
||||
}
|
104
app/Console/Commands/BBS/FrameImport.php
Normal file
104
app/Console/Commands/BBS/FrameImport.php
Normal 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));
|
||||
}
|
||||
}
|
128
app/Console/Commands/BBS/Start.php
Normal file
128
app/Console/Commands/BBS/Start.php
Normal 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));
|
||||
}
|
||||
}
|
53
app/Console/Commands/CommBinkpReceive.php
Normal file
53
app/Console/Commands/CommBinkpReceive.php
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
Job::dispatch($ao,$mo);
|
||||
}
|
||||
}
|
||||
|
53
app/Console/Commands/CommEMSIReceive.php
Normal file
53
app/Console/Commands/CommEMSIReceive.php
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
Job::dispatch($ao,$mo);
|
||||
}
|
||||
}
|
||||
|
@@ -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(',')));
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
@@ -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+');
|
||||
|
@@ -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,22 +25,21 @@ 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) {
|
||||
return [
|
||||
'id'=>$item->id,
|
||||
'region_id'=>$item->region_id,
|
||||
'ftn'=>$item->ftn4d,
|
||||
'role'=>$item->role_name,
|
||||
'parent'=>$item->parent()?->ftn4d,
|
||||
'children'=>$item->children()->count(),
|
||||
'downlinks'=>$item->downlinks()->count(),
|
||||
'uplink'=>($x=$item->uplink())?->ftn4d,
|
||||
'send from'=>$x ? our_address($item->uplink())?->ftn4d : '',
|
||||
'system'=>$item->system->name,
|
||||
'notes'=>$item->isRoleOverride() ? 'Role Override' : '',
|
||||
];
|
||||
}));
|
||||
$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,
|
||||
'ftn'=>$item->ftn4d,
|
||||
'role'=>$item->role_name,
|
||||
'parent'=>$item->parent()?->ftn4d,
|
||||
'children'=>$item->children()->count(),
|
||||
'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' : '',
|
||||
];
|
||||
}));
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
|
@@ -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'));
|
||||
}
|
||||
}
|
@@ -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'));
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -36,15 +36,22 @@ class NodelistImport extends Command
|
||||
*/
|
||||
public function handle():int
|
||||
{
|
||||
return Job::dispatchSync(
|
||||
is_numeric($x=$this->argument('file'))
|
||||
? File::findOrFail($x)
|
||||
: sprintf('%s/%s',config('fido.dir'),$this->argument('file')),
|
||||
$this->argument('domain'),
|
||||
$this->option('delete'),
|
||||
$this->option('unlink'),
|
||||
$this->option('test'),
|
||||
$this->option('ignorecrc'),
|
||||
);
|
||||
try {
|
||||
return Job::dispatchSync(
|
||||
is_numeric($x=$this->argument('file'))
|
||||
? File::findOrFail($x)
|
||||
: sprintf('%s/%s',config('fido.dir'),$this->argument('file')),
|
||||
$this->argument('domain'),
|
||||
$this->option('delete'),
|
||||
$this->option('unlink'),
|
||||
$this->option('test'),
|
||||
$this->option('ignorecrc'),
|
||||
);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e->getMessage());
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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));
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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
Reference in New Issue
Block a user