Compare commits
270 Commits
1528657e9c
...
1e617bb8be
Author | SHA1 | Date | |
---|---|---|---|
1e617bb8be | |||
b43784574c | |||
364815e8af | |||
3d43a256ba | |||
b460cd0196 | |||
643f1197d6 | |||
7ef9f2dbd0 | |||
b102fc4d2a | |||
e15331ec35 | |||
3a594acc03 | |||
800593d034 | |||
65b2a2d519 | |||
bf21671a1f | |||
fe18968c57 | |||
b6639c7bfc | |||
7e0178d183 | |||
03bfc9dbfc | |||
77b9bb30c4 | |||
f8cb6ccc37 | |||
0e1d5b3239 | |||
9f3b9f692a | |||
391e9e1e39 | |||
3555e5a91c | |||
2f24e13940 | |||
87f495b326 | |||
1615b413a7 | |||
86c27a3f17 | |||
a687b5fd1c | |||
27956146e3 | |||
a547e29e56 | |||
de34052c3b | |||
cb63ec50d2 | |||
8b00d29db3 | |||
f082bb0ebd | |||
4f8448563d | |||
710adad634 | |||
5fc69067fb | |||
dc212d35fb | |||
3ce6a8ed61 | |||
b398163cfd | |||
a5e9a28673 | |||
49e40f4fb8 | |||
924d760c79 | |||
58dc090c83 | |||
b20878e378 | |||
b443762739 | |||
18f5354d0c | |||
17e3c69f07 | |||
51784df6a8 | |||
8df6384736 | |||
752462d20f | |||
72d68fa1ab | |||
aaec5f8f4a | |||
ab2e288f06 | |||
b30ab2f999 | |||
b4a42f6780 | |||
f279d85b08 | |||
46f52dd56d | |||
59ec5f5a0c | |||
29710c37c2 | |||
1650d07d5c | |||
0457b3df25 | |||
731fdb0a44 | |||
6216ada5e5 | |||
b9b5cf4214 | |||
f912e81ee6 | |||
4fe5dc6ad0 | |||
556b95c7c1 | |||
14c505c15b | |||
dd8558487c | |||
3ad20f969b | |||
4d13199848 | |||
cd2efbd1d4 | |||
edee0643ec | |||
23159d19d5 | |||
2765a27db8 | |||
c8ef7d065b | |||
7540ddf8f4 | |||
b17fe1d2ee | |||
5389739920 | |||
92f964f572 | |||
9abfd88e3d | |||
e9895aee45 | |||
79b180f453 | |||
6e376100a5 | |||
1a5c1eff7b | |||
f42fe97902 | |||
ac02f37c67 | |||
6a41536d57 | |||
527cc1d4ab | |||
d90f431925 | |||
ceffc7ff14 | |||
001618d719 | |||
bba6f93fbc | |||
1c270025cf | |||
8bf58f3daa | |||
3f5668292f | |||
20d3776490 | |||
7b225d8fc0 | |||
ff7ab68a54 | |||
a2ff2df9f3 | |||
9c9fd84e0a | |||
5cb70da458 | |||
3e561ab068 | |||
42cc50512f | |||
bb42f418e0 | |||
ae0bd09a47 | |||
9299697ec1 | |||
bac41969a5 | |||
d6e23b9a90 | |||
2edc41b11e | |||
03ca4c10b1 | |||
1923eb429f | |||
1e08c2f6f7 | |||
77df5746be | |||
60964e27a7 | |||
c496d131cf | |||
2ba656b1d9 | |||
cb09016539 | |||
0c17391dec | |||
c1a1797778 | |||
9376c6de11 | |||
713615d8d5 | |||
01107cd3dc | |||
0e5a04596a | |||
c9d04b64ac | |||
90206f2bb5 | |||
ac2ee7df0c | |||
24f6af3d3b | |||
9c8e546765 | |||
13e51724c0 | |||
1ded66990c | |||
6fb7d165ae | |||
fd07fb2be7 | |||
e1c9fa12aa | |||
7b9ab388d8 | |||
7af67de2a8 | |||
6d9179ed37 | |||
27c050dc38 | |||
301fc33d2f | |||
5d88a5e10e | |||
aae551aacf | |||
26c80dc1c5 | |||
ba0d612889 | |||
f6a6c13ca2 | |||
b9bc413b05 | |||
caa6e629f4 | |||
541f612446 | |||
247cf614f3 | |||
ab5476d373 | |||
0526500ff0 | |||
8fc0336314 | |||
5a74386f5a | |||
e56ecaa999 | |||
ddccc44261 | |||
fa2e74eaca | |||
ee15274478 | |||
1890b66dc7 | |||
8f3d77b04d | |||
049b2c7204 | |||
5b7ec1a629 | |||
535a082edd | |||
9cf0f1e2f4 | |||
27a3e3e24e | |||
8590bb8acc | |||
a19eaa3291 | |||
4a0e6e67fc | |||
1ac3583479 | |||
6e7e09ab50 | |||
2b2482ba71 | |||
b5e5decfdf | |||
6abf10ab0b | |||
4070a060c3 | |||
a13497df5f | |||
82cee02fa8 | |||
e337a29003 | |||
eab35d4c18 | |||
4c91ed54c0 | |||
bed5bf8acc | |||
9e870858da | |||
0800c48928 | |||
455fed52ee | |||
19338edcb6 | |||
b3dfca5b89 | |||
76dc90ceb3 | |||
a13028808a | |||
e5de4970d1 | |||
7847728e52 | |||
b8670a5593 | |||
fbcbe2c5a8 | |||
5f11f81be3 | |||
fcc2c23894 | |||
9fd8264c3f | |||
5b24ff944f | |||
3221d7f679 | |||
f639e3ffab | |||
e8f4bf93bd | |||
116f726885 | |||
a74c5d5f5c | |||
3a0847f13a | |||
509cdd7ea6 | |||
45d78233b2 | |||
4a870b6587 | |||
67747c062a | |||
250e584c03 | |||
7087fe9bbb | |||
3b99c409e0 | |||
ea42a347eb | |||
85243d128e | |||
7d82cbcf12 | |||
a886a389a8 | |||
f9d24db9f8 | |||
8ce3ce8164 | |||
a7e8cc7568 | |||
c8ab8d3db3 | |||
953d3725b2 | |||
8332f485d1 | |||
b32020e60f | |||
2c504c3d66 | |||
df5cc8c2d4 | |||
4616feacda | |||
7a9b6d5015 | |||
654e7bd2aa | |||
b25e6f432c | |||
27985dbf0b | |||
fda68bba04 | |||
c86d8d8952 | |||
614d332fae | |||
0cabdcd3c1 | |||
495a27cfed | |||
32c0088339 | |||
b854cf9fe0 | |||
ce7a96ca2a | |||
28e30a05e6 | |||
e75be34afd | |||
d82f8ac8b3 | |||
0fcb628c11 | |||
c7e707c143 | |||
62f0c1a909 | |||
073d95f605 | |||
2a50a1d795 | |||
782acad560 | |||
c0c8861c08 | |||
d11a2a5b8d | |||
ff04de52b5 | |||
56544b89e1 | |||
2ae24b9955 | |||
22c8b3df74 | |||
2e7aecff57 | |||
b7c1c97cf7 | |||
612efda945 | |||
7fedf88d8c | |||
11f9adf11a | |||
eebe8a159d | |||
3a35bce9e7 | |||
5e67be5ba1 | |||
f315c71ca9 | |||
4343774079 | |||
eb40f94e37 | |||
cc04ddd7b3 | |||
e611dcbe11 | |||
073fa466d6 | |||
708d9a9f67 | |||
c1d6d48a3c | |||
6e133770fc | |||
a991db788e | |||
096e37ef35 | |||
2f878b6e64 | |||
ec5c28a03e | |||
ff8c370d86 |
@ -2,7 +2,8 @@ APP_NAME="Clearing Houz"
|
||||
APP_ENV=production
|
||||
APP_KEY=
|
||||
APP_DEBUG=false
|
||||
APP_URL=http://localhost
|
||||
APP_URL=http://clrghouz
|
||||
APP_TIMEZONE=
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_LEVEL=info
|
||||
@ -10,8 +11,8 @@ LOG_LEVEL=info
|
||||
DB_CONNECTION=pgsql
|
||||
DB_HOST=postgres
|
||||
DB_PORT=5432
|
||||
DB_DATABASE=laravel
|
||||
DB_USERNAME=laravel
|
||||
DB_DATABASE=clrghouz
|
||||
DB_USERNAME=clrghouz
|
||||
DB_PASSWORD=
|
||||
#DB_SSLMODE=prefer
|
||||
#DB_SSLROOTCERT=/var/www/html/config/ssl/ca.crt
|
||||
@ -19,6 +20,7 @@ DB_PASSWORD=
|
||||
#DB_SSLKEY=/var/www/html/config/ssl/client.key
|
||||
|
||||
BROADCAST_DRIVER=log
|
||||
MEMCACHED_HOST=memcached
|
||||
CACHE_DRIVER=memcached
|
||||
QUEUE_CONNECTION=database
|
||||
SESSION_DRIVER=file
|
||||
@ -46,6 +48,7 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
|
||||
FIDO_DIR=fido
|
||||
FIDO_PACKET_KEEP=
|
||||
FIDO_STRICT=false
|
||||
|
||||
FILESYSTEM_DISK=s3
|
||||
AWS_ACCESS_KEY_ID=
|
||||
|
@ -2,7 +2,7 @@ APP_NAME="Clearing Houz Testing"
|
||||
APP_ENV=testing
|
||||
APP_KEY=base64:FiMSvv4J7jDfy6W/sHrQ9YImuUYaxynYCcXQJwp/6Tc=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
APP_URL=http://clrghouz
|
||||
APP_TIMEZONE=Australia/Melbourne
|
||||
|
||||
LOG_CHANNEL=stderr
|
||||
@ -41,7 +41,8 @@ PUSHER_APP_CLUSTER=mt1
|
||||
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
|
||||
FIDO_DIR=fido
|
||||
FIDO_DIR=test
|
||||
FIDO_DIR_FILES=local
|
||||
FIDO_PACKET_KEEP=true
|
||||
|
||||
FILESYSTEM_DISK=s3
|
||||
|
147
.gitea/workflows/build_docker.yaml
Normal file
147
.gitea/workflows/build_docker.yaml
Normal file
@ -0,0 +1,147 @@
|
||||
name: Create Docker Image
|
||||
run-name: ${{ gitea.actor }} Building Docker Image 🐳
|
||||
on: [push]
|
||||
env:
|
||||
VERSION: latest
|
||||
DOCKER_HOST: tcp://127.0.0.1:2375
|
||||
|
||||
jobs:
|
||||
# test:
|
||||
# strategy:
|
||||
# matrix:
|
||||
# arch:
|
||||
# - x86_64
|
||||
# # arm64
|
||||
#
|
||||
# name: Test Application
|
||||
# runs-on: docker-${{ matrix.arch }}
|
||||
# container:
|
||||
# image: gitea.dege.au/docker/php:8.3-fpm-pgsql-server-test
|
||||
#
|
||||
# steps:
|
||||
# - name: Environment Setup
|
||||
# run: |
|
||||
# # If we have a proxy use it
|
||||
# if [ -n "${HTTP_PROXY}" ]; then echo "HTTP PROXY [${HTTP_PROXY}]"; sed -i -e s'/https/http/' /etc/apk/repositories; fi
|
||||
# # Some pre-reqs
|
||||
# apk add git nodejs
|
||||
# ## Some debugging info
|
||||
# # env|sort
|
||||
#
|
||||
# - name: Code Checkout
|
||||
# uses: actions/checkout@v4
|
||||
#
|
||||
# - name: Run Tests
|
||||
# run: |
|
||||
# mv .env.testing .env
|
||||
# # Install Composer and project dependencies.
|
||||
# mkdir -p ${COMPOSER_HOME}
|
||||
# if [ -n "${{ secrets.COMPOSER_GITHUB_TOKEN }}" ]; then echo ${{ secrets.COMPOSER_GITHUB_TOKEN }} > ${COMPOSER_HOME}/auth.json; fi
|
||||
# composer install
|
||||
# # Generate an application key. Re-cache.
|
||||
# php artisan key:generate
|
||||
# php artisan migrate
|
||||
# php artisan db:seed
|
||||
# # run laravel tests
|
||||
# touch storage/app/test/*ZIP storage/app/test/file/*
|
||||
# XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text --colors=never
|
||||
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
arch:
|
||||
- x86_64
|
||||
- arm64
|
||||
# needs: [test]
|
||||
|
||||
name: Build Docker Image
|
||||
runs-on: docker-${{ matrix.arch }}
|
||||
container:
|
||||
image: docker:dind
|
||||
privileged: true
|
||||
env:
|
||||
ARCH: ${{ matrix.arch }}
|
||||
VERSIONARCH: ${{ env.VERSION }}-${{ env.ARCH }}
|
||||
|
||||
steps:
|
||||
- name: Environment Setup
|
||||
run: |
|
||||
# If we have a proxy use it
|
||||
if [ -n "${HTTP_PROXY}" ]; then echo "HTTP PROXY [${HTTP_PROXY}]"; sed -i -e s'/https/http/' /etc/apk/repositories; fi
|
||||
# Some pre-reqs
|
||||
apk add git curl nodejs
|
||||
# Start docker
|
||||
( dockerd --host=tcp://0.0.0.0:2375 --tls=false & ) && sleep 3
|
||||
## Some debugging info
|
||||
# docker info && docker version
|
||||
env|sort
|
||||
echo "PRT: ${{ secrets.PKG_WRITE_TOKEN }}"
|
||||
|
||||
- name: Registry FQDN Setup
|
||||
id: registry
|
||||
run: |
|
||||
registry=${{ github.server_url }}
|
||||
echo "registry=${registry##http*://}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Container Registry Login
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ steps.registry.outputs.registry }}
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.PKG_WRITE_TOKEN }}
|
||||
|
||||
- name: Code Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Record version
|
||||
run: |
|
||||
pwd
|
||||
ls -al
|
||||
echo ${GITHUB_SHA::8} > VERSION
|
||||
cat VERSION
|
||||
|
||||
- name: Build and Push Docker Image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
push: true
|
||||
tags: "${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSIONARCH }}"
|
||||
|
||||
manifest:
|
||||
name: Final Docker Image Manifest
|
||||
runs-on: docker-x86_64
|
||||
container:
|
||||
image: docker:dind
|
||||
privileged: true
|
||||
needs: [build]
|
||||
|
||||
steps:
|
||||
- name: Environment Setup
|
||||
run: |
|
||||
# If we have a proxy use it
|
||||
if [ -n "${HTTP_PROXY}" ]; then echo "HTTP PROXY [${HTTP_PROXY}]"; sed -i -e s'/https/http/' /etc/apk/repositories; fi
|
||||
# Some pre-reqs
|
||||
apk add git curl nodejs
|
||||
# Start docker
|
||||
( dockerd --host=tcp://0.0.0.0:2375 --tls=false & ) && sleep 3
|
||||
|
||||
- name: Registry FQDN Setup
|
||||
id: registry
|
||||
run: |
|
||||
registry=${{ github.server_url }}
|
||||
echo "registry=${registry##http*://}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Container Registry Login
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ steps.registry.outputs.registry }}
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.PKG_WRITE_TOKEN }}
|
||||
|
||||
- name: Build Docker Manifest
|
||||
run: |
|
||||
docker manifest create ${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSION }} \
|
||||
${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSION }}-x86_64 \
|
||||
${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSION }}-arm64
|
||||
docker manifest push --purge ${{ steps.registry.outputs.registry }}/${{ env.GITHUB_REPOSITORY }}:${{ env.VERSION }}
|
@ -1,14 +0,0 @@
|
||||
stages:
|
||||
- test
|
||||
- build
|
||||
|
||||
# This folder is cached between builds
|
||||
# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
|
||||
cache:
|
||||
key: ${CI_JOB_NAME_SLUG}-${CI_COMMIT_REF_SLUG}
|
||||
paths:
|
||||
- vendor/
|
||||
|
||||
include:
|
||||
- .gitlab-test.yml
|
||||
- .gitlab-docker-x86_64.yml
|
@ -1,27 +0,0 @@
|
||||
docker:
|
||||
variables:
|
||||
VERSION: latest
|
||||
DOCKER_HOST: tcp://docker:2375
|
||||
|
||||
stage: build
|
||||
|
||||
image: docker:latest
|
||||
services:
|
||||
- docker:dind
|
||||
|
||||
before_script:
|
||||
- docker info && docker version
|
||||
- echo "$CI_JOB_TOKEN" | docker login -u "$CI_REGISTRY_USER" "$CI_REGISTRY" --password-stdin
|
||||
- if [ -n "$GITHUB_TOKEN" ]; then cat $GITHUB_TOKEN |base64 -d > auth.json; fi
|
||||
|
||||
script:
|
||||
- if [ -f init ]; then chmod 500 init; fi
|
||||
- echo -n ${CI_COMMIT_SHORT_SHA} > VERSION
|
||||
- rm -rf vendor/ database/schema database/seeders database/factories/*
|
||||
- docker build -t ${CI_REGISTRY_IMAGE}:${VERSION} .
|
||||
- docker push ${CI_REGISTRY_IMAGE}:${VERSION}
|
||||
tags:
|
||||
- docker
|
||||
- x86_64
|
||||
only:
|
||||
- master
|
@ -1,42 +0,0 @@
|
||||
test:
|
||||
image: ${CI_REGISTRY}/leenooks/php:8.1-fpm-alpine-pgsql-server-test
|
||||
|
||||
stage: test
|
||||
|
||||
# NOTE: This service is dependant on project file configuration, which is not there if the cache was deleted
|
||||
# resulting in the testing to fail on the first run.
|
||||
services:
|
||||
- name: postgres:15-alpine
|
||||
alias: postgres-test
|
||||
|
||||
variables:
|
||||
POSTGRES_USER: test
|
||||
POSTGRES_PASSWORD: test
|
||||
|
||||
tags:
|
||||
- php
|
||||
only:
|
||||
- master
|
||||
|
||||
before_script:
|
||||
- mv .env.testing .env
|
||||
|
||||
# Install Composer and project dependencies.
|
||||
- mkdir -p ${COMPOSER_HOME}
|
||||
- if [ -n "$GITHUB_TOKEN" ]; then cat $GITHUB_TOKEN |base64 -d > ${COMPOSER_HOME}/auth.json; fi
|
||||
- composer install
|
||||
|
||||
# Generate an application key. Re-cache.
|
||||
- php artisan key:generate
|
||||
- php artisan migrate
|
||||
- php artisan db:seed
|
||||
|
||||
script:
|
||||
# run laravel tests
|
||||
- XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text --colors=never
|
||||
|
||||
# run frontend tests
|
||||
# if you have any task for testing frontend
|
||||
# set it in your package.json script
|
||||
# comment this out if you don't have a frontend test
|
||||
# npm test
|
@ -28,7 +28,13 @@ class CompressedString implements CastsAttributes
|
||||
? stream_get_contents($value)
|
||||
: $value;
|
||||
|
||||
return $value ? zstd_uncompress(base64_decode($value)) : '';
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
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];
|
||||
}
|
||||
}
|
12
app/Classes/Dynamic.php
Normal file
12
app/Classes/Dynamic.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes;
|
||||
|
||||
/**
|
||||
* Dynamic files that are sent to systems during a mailer session
|
||||
*/
|
||||
abstract class Dynamic
|
||||
{
|
||||
abstract public function __toString(): string;
|
||||
abstract public function getName(): string;
|
||||
}
|
125
app/Classes/Dynamic/HubStats.php
Normal file
125
app/Classes/Dynamic/HubStats.php
Normal file
@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Dynamic;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
use App\Classes\Dynamic;
|
||||
use App\Models\Address;
|
||||
|
||||
/**
|
||||
* This method will generate the hub status for an upstream Host/RC/ZC
|
||||
*
|
||||
* Arg is a collection of arguments. We only understand
|
||||
* + name - name of file to give to remote
|
||||
*/
|
||||
class HubStats extends Dynamic
|
||||
{
|
||||
private const LOGKEY = 'DHS';
|
||||
|
||||
private string $name = '';
|
||||
|
||||
public function __construct(private Address $ao,Collection $arg)
|
||||
{
|
||||
Log::debug(sprintf('%s:- Generating Hub Stats for [%s] with arguments',self::LOGKEY,$ao->ftn),['args'=>$arg]);
|
||||
$this->name = $arg->get('name');
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
$date = Carbon::now()->yesterday()->endOfday();
|
||||
|
||||
$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";
|
||||
|
||||
$output = sprintf("Hub Status for [%s] as at [%s]\r\n",our_address($this->ao)->ftn,$date);
|
||||
$output .= "\r";
|
||||
$output .= "+--------------+------+-----+-----+------------------+-------+-------+\r\n";
|
||||
$output .= "| FTN | ECHO | NET |FILES| LAST SESSION | MODE |AUTOHLD|\r\n";
|
||||
$output .= "+--------------+------+-----+-----+------------------+-------+-------+\r\n";
|
||||
|
||||
$havedown = FALSE;
|
||||
$havehold = FALSE;
|
||||
|
||||
foreach($r->get() as $o) {
|
||||
if ($o->uncollected_echomail > 10000)
|
||||
$o->uncollected_echomail = 9999;
|
||||
|
||||
if ($o->uncollected_netmail > 10000)
|
||||
$o->uncollected_netmail = 9999;
|
||||
|
||||
if ($o->uncollected_files > 10000)
|
||||
$o->uncollected_files = 9999;
|
||||
|
||||
if ((! $havedown) && $o->is_down)
|
||||
$havedown = TRUE;
|
||||
|
||||
if ((! $havehold) && $o->is_hold)
|
||||
$havehold = TRUE;
|
||||
|
||||
$output .= sprintf($header,
|
||||
sprintf('%s %s',$o->ftn4d,$o->is_down ? 'd' : ($o->is_hold ? 'h' : ' ')),
|
||||
$o->uncollected_echomail ?? 0,
|
||||
$o->uncollected_netmail ?? 0,
|
||||
$o->uncollected_files ?? 0,
|
||||
$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');
|
||||
}
|
||||
|
||||
$output .= "+--------------+------+-----+-----+------------------+-------+-------+\r\n";
|
||||
|
||||
$output .= "\r\n";
|
||||
|
||||
if ($havehold)
|
||||
$output .= "(h) Node is on HOLD status.\r\n";
|
||||
|
||||
if ($havedown)
|
||||
$output .= "(d) Node is on DOWN status.\r\n";
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name ?: 'hubstats.txt';
|
||||
}
|
||||
}
|
162
app/Classes/Dynamic/NodelistSegment.php
Normal file
162
app/Classes/Dynamic/NodelistSegment.php
Normal file
@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Dynamic;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
use App\Classes\Dynamic;
|
||||
use App\Models\{Address,System};
|
||||
|
||||
/**
|
||||
* This method will generate a nodelist for an upstream Host/RC/ZC/NC
|
||||
*
|
||||
* // @todo Only generate if nodes have been updated, added or removed
|
||||
*/
|
||||
class NodelistSegment extends Dynamic
|
||||
{
|
||||
private const LOGKEY = 'DNL';
|
||||
|
||||
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)->first();
|
||||
$this->now = Carbon::now();
|
||||
|
||||
$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]);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->crc($this->generate($this->our_address));
|
||||
}
|
||||
|
||||
private function crc(string $text): string
|
||||
{
|
||||
return sprintf(";A %s Nodelist for %s -- Day number %d : %s\r\n",
|
||||
$this->our_address->role_name,
|
||||
$this->now->format('l, M d, Y'),
|
||||
$this->now->format('z'),
|
||||
crc16($text)
|
||||
).$text;
|
||||
}
|
||||
|
||||
private function flags(Address $ao): Collection
|
||||
{
|
||||
$result = collect();
|
||||
|
||||
if (($ao->system->pollmode === TRUE) || in_array($ao->role_name,['ZC','RC','NC','HC']))
|
||||
$result->push('CM');
|
||||
|
||||
if ($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 : ''));
|
||||
|
||||
if (($x=$ao->system->mailers->pluck('name')->search('EMSI')) !== FALSE)
|
||||
$result->push(sprintf('ITN%s',(($y=$ao->system->mailers->get($x)->pivot->port) !== 23) ? ':'.$y : ''));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function entry(Address $ao): string
|
||||
{
|
||||
$format = '%s,%d,%s,%s,%s,%s,300';
|
||||
|
||||
$prefix = '';
|
||||
$address = $ao->node_id;
|
||||
|
||||
if ((! $ao->system->address) || $ao->isPrivate) {
|
||||
$prefix = 'Pvt';
|
||||
|
||||
} elseif ($ao->isHold) {
|
||||
$prefix = 'Hold';
|
||||
|
||||
} elseif ($ao->isDown) {
|
||||
$prefix = 'Down';
|
||||
|
||||
} else
|
||||
switch ($ao->role_id) {
|
||||
case Address::NODE_ZC:
|
||||
$prefix = 'Zone';
|
||||
$address = $ao->zone->zone_id;
|
||||
break;
|
||||
|
||||
case Address::NODE_RC:
|
||||
$prefix = 'Region';
|
||||
$address = $ao->region_id;
|
||||
break;
|
||||
|
||||
case Address::NODE_NC:
|
||||
$prefix = 'Host';
|
||||
$address = $ao->host_id;
|
||||
break;
|
||||
|
||||
case Address::NODE_HC:
|
||||
$prefix = 'Hub';
|
||||
break;
|
||||
|
||||
case Address::NODE_NN:
|
||||
break;
|
||||
|
||||
case Address::NODE_POINT:
|
||||
throw new \Exception(sprintf('We have no method to include points in the nodelist [%s]',$ao->ftn));
|
||||
|
||||
default:
|
||||
throw new \Exception(sprintf('Unknown role [%d] for [%s]',$ao->role,$ao->ftn));
|
||||
}
|
||||
|
||||
return sprintf($format,
|
||||
$prefix,
|
||||
$address,
|
||||
$this->format($ao->system->name),
|
||||
$this->format($ao->system->location),
|
||||
$this->format($ao->system->sysop),
|
||||
$ao->system->phone ?: '-Unpublished-',
|
||||
).(($x=$this->flags($ao)->join(',')) ? sprintf(',%s',$x) : '');
|
||||
}
|
||||
|
||||
private function format(string $string): string
|
||||
{
|
||||
return str_replace(' ','_',str_replace(',','',$string));
|
||||
}
|
||||
|
||||
private function generate(Address $ao): string
|
||||
{
|
||||
$result = collect();
|
||||
$so = System::createUnknownSystem();
|
||||
|
||||
$result->push($this->entry($ao));
|
||||
|
||||
foreach ($ao->children() as $oo) {
|
||||
// We dont include points in the ndoelist
|
||||
if ($oo->point_id)
|
||||
continue;
|
||||
|
||||
// We exclude discovered systems
|
||||
if ($oo->system_id == $so->id)
|
||||
continue;
|
||||
|
||||
$result->push($this->generate($oo) ?: $this->entry($oo));
|
||||
}
|
||||
|
||||
return $result->join("\n");
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ abstract class FTN
|
||||
public function __get(string $key)
|
||||
{
|
||||
switch ($key) {
|
||||
case 'fftn':
|
||||
case 'fftn_t':
|
||||
return sprintf('%d:%d/%d.%d',
|
||||
$this->fz,
|
||||
$this->fn,
|
||||
@ -19,7 +19,7 @@ abstract class FTN
|
||||
$this->fp,
|
||||
).($this->zone ? sprintf('@%s',$this->zone->domain->name) : '');
|
||||
|
||||
case 'tftn':
|
||||
case 'tftn_t':
|
||||
return sprintf('%d:%d/%d.%d',
|
||||
$this->tz,
|
||||
$this->tn,
|
||||
@ -27,30 +27,16 @@ abstract class FTN
|
||||
$this->tp,
|
||||
).($this->zone ? sprintf('@%s',$this->zone->domain->name) : '');
|
||||
|
||||
case 'fftn_o':
|
||||
return Address::findFTN($this->fftn);
|
||||
case 'tftn_o':
|
||||
return Address::findFTN($this->tftn);
|
||||
case 'fftn':
|
||||
return Address::findFTN($this->fftn_t);
|
||||
case 'tftn':
|
||||
return Address::findFTN($this->tftn_t);
|
||||
|
||||
default:
|
||||
throw new \Exception('Unknown key: '.$key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a line is a kludge line.
|
||||
*
|
||||
* @param string $kludge
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
protected function kludge(string $kludge,string $string)
|
||||
{
|
||||
return (preg_match("/^{$kludge}/",$string))
|
||||
? chop(preg_replace("/^{$kludge}/",'',$string),"\r")
|
||||
: FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function creates our unpack header
|
||||
*
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -5,23 +5,38 @@ namespace App\Classes\FTN;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Symfony\Component\HttpFoundation\File\File;
|
||||
|
||||
use App\Classes\FTN as FTNBase;
|
||||
use App\Models\{Address,Software,System,Zone};
|
||||
use App\Exceptions\InvalidPacketException;
|
||||
use App\Models\{Address,Domain,Echomail,Netmail,Software,System,Zone};
|
||||
use App\Notifications\Netmails\EchomailBadAddress;
|
||||
|
||||
/**
|
||||
* Represents the structure of a message bundle
|
||||
* Represents a Fidonet Packet, that contains an array of messages.
|
||||
*
|
||||
* Thus this object is iterable as an array of Echomail::class or Netmail::class.
|
||||
*/
|
||||
class Packet extends FTNBase implements \Iterator, \Countable
|
||||
abstract class Packet extends FTNBase implements \Iterator, \Countable
|
||||
{
|
||||
private const LOGKEY = 'PKT';
|
||||
|
||||
private const BLOCKSIZE = 1024;
|
||||
protected const PACKED_MSG_LEAD = "\02\00";
|
||||
protected const PACKED_END = "\00\00";
|
||||
|
||||
public const MSG_TYPE2 = 1<<0;
|
||||
public const MSG_TYPE4 = 1<<2;
|
||||
|
||||
// @todo Rename this regex to something more descriptive, ie: FILENAME_REGEX
|
||||
public const regex = '([[:xdigit:]]{4})(?:-(\d{4,10}))?-(.+)';
|
||||
|
||||
/**
|
||||
* Packet types we support, in specific order for auto-detection to work
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public const PACKET_TYPES = [
|
||||
'2.2' => FTNBase\Packet\FSC45::class,
|
||||
'2+' => FTNBase\Packet\FSC48::class,
|
||||
@ -30,127 +45,17 @@ class Packet extends FTNBase implements \Iterator, \Countable
|
||||
];
|
||||
|
||||
protected array $header; // Packet Header
|
||||
protected ?string $name; // Packet name
|
||||
protected ?string $name = NULL; // Packet name
|
||||
|
||||
public File $file; // Packet filename
|
||||
public Collection $messages; // Messages in the Packet
|
||||
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
|
||||
public Collection $errors; // Messages that fail validation
|
||||
public bool $use_cache = FALSE; // Use a cache for messages.
|
||||
protected int $index; // Our array index
|
||||
protected int $index; // Our array index
|
||||
protected $pass_p = NULL; // Overwrite the packet password (when packing messages)
|
||||
|
||||
/**
|
||||
* @param string|null $header
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct(string $header=NULL)
|
||||
{
|
||||
$this->messages = collect();
|
||||
$this->errors = collect();
|
||||
$this->domain = NULL;
|
||||
$this->name = NULL;
|
||||
|
||||
if ($header)
|
||||
$this->header = unpack(self::unpackheader(static::HEADER),$header);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __get($key)
|
||||
{
|
||||
switch ($key) {
|
||||
// From Addresses
|
||||
case 'fz': return Arr::get($this->header,'ozone');
|
||||
case 'fn': return Arr::get($this->header,'onet');
|
||||
case 'ff': return Arr::get($this->header,'onode');
|
||||
case 'fp': return Arr::get($this->header,'opoint');
|
||||
case 'fd': return rtrim(Arr::get($this->header,'odomain',"\x00"));
|
||||
|
||||
// To Addresses
|
||||
case 'tz': return Arr::get($this->header,'dzone');
|
||||
case 'tn': return Arr::get($this->header,'dnet');
|
||||
case 'tf': return Arr::get($this->header,'dnode');
|
||||
case 'tp': return Arr::get($this->header,'dpoint');
|
||||
case 'td': return rtrim(Arr::get($this->header,'ddomain',"\x00"));
|
||||
|
||||
case 'date':
|
||||
return Carbon::create(
|
||||
Arr::get($this->header,'y'),
|
||||
Arr::get($this->header,'m')+1,
|
||||
Arr::get($this->header,'d'),
|
||||
Arr::get($this->header,'H'),
|
||||
Arr::get($this->header,'M'),
|
||||
Arr::get($this->header,'S')
|
||||
);
|
||||
|
||||
case 'password':
|
||||
return rtrim(Arr::get($this->header,$key),"\x00");
|
||||
|
||||
case 'fftn':
|
||||
case 'fftn_o':
|
||||
case 'tftn':
|
||||
case 'tftn_o':
|
||||
return parent::__get($key);
|
||||
|
||||
case 'software':
|
||||
$code = Arr::get($this->header,'prodcode-hi')<<8|Arr::get($this->header,'prodcode-lo');
|
||||
Software::unguard();
|
||||
$o = Software::singleOrNew(['code'=>$code,'type'=>Software::SOFTWARE_TOSSER]);
|
||||
Software::reguard();
|
||||
|
||||
return $o;
|
||||
|
||||
case 'software_ver':
|
||||
return sprintf('%d.%d',Arr::get($this->header,'prodrev-maj'),Arr::get($this->header,'prodrev-min'));
|
||||
|
||||
case 'capability':
|
||||
// This needs to be defined in child classes, since not all children have it
|
||||
return NULL;
|
||||
|
||||
// Packet Type
|
||||
case 'type':
|
||||
return static::TYPE;
|
||||
|
||||
// Packet name:
|
||||
case 'name':
|
||||
return $this->{$key} ?: sprintf('%08x',timew());
|
||||
|
||||
default:
|
||||
throw new \Exception('Unknown key: '.$key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the packet
|
||||
*
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
$return = $this->header();
|
||||
|
||||
foreach ($this->messages as $o) {
|
||||
if ($o->packed)
|
||||
$return .= self::PACKED_MSG_LEAD.$o;
|
||||
}
|
||||
|
||||
$return .= "\00\00";
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/* STATIC */
|
||||
|
||||
/**
|
||||
* Site of the packet header
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function header_len(): int
|
||||
{
|
||||
return collect(static::HEADER)->sum(function($item) { return Arr::get($item,2); });
|
||||
}
|
||||
/* ABSTRACT */
|
||||
|
||||
/**
|
||||
* This function is intended to be implemented in child classes to test if the packet
|
||||
@ -160,23 +65,32 @@ class Packet extends FTNBase implements \Iterator, \Countable
|
||||
* @param string $header
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_type(string $header): bool
|
||||
abstract public static function is_type(string $header): bool;
|
||||
abstract protected function header(): string;
|
||||
|
||||
/* STATIC */
|
||||
|
||||
/**
|
||||
* Size of the packet header
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function header_len(): int
|
||||
{
|
||||
return FALSE;
|
||||
return collect(static::HEADER)->sum(function($item) { return Arr::get($item,2); });
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a packet file
|
||||
*
|
||||
* @param mixed $f
|
||||
* @param mixed $f File handler returning packet data
|
||||
* @param string $name
|
||||
* @param int $size
|
||||
* @param System|null $system
|
||||
* @param bool $use_cache
|
||||
* @param Domain|null $domain
|
||||
* @return Packet
|
||||
* @throws InvalidPacketException
|
||||
*/
|
||||
public static function process(mixed $f,string $name,int $size,System $system=NULL,bool $use_cache=FALSE): 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));
|
||||
|
||||
@ -207,123 +121,185 @@ class Packet extends FTNBase implements \Iterator, \Countable
|
||||
if (! $o)
|
||||
throw new InvalidPacketException('Cannot determine type of packet.');
|
||||
|
||||
$o->use_cache = $use_cache;
|
||||
$o->name = $name;
|
||||
|
||||
$x = fread($f,2);
|
||||
if (strlen($x) === 2) {
|
||||
// End of Packet?
|
||||
if ($x === "\00\00")
|
||||
return $o;
|
||||
|
||||
// End of Packet?
|
||||
if ((strlen($x) === 2) && ($x === "\00\00"))
|
||||
return $o;
|
||||
|
||||
// Messages start with self::PACKED_MSG_LEAD
|
||||
if ((strlen($x) === 2) && ($x !== self::PACKED_MSG_LEAD))
|
||||
throw new InvalidPacketException('Not a valid packet: '.bin2hex($x));
|
||||
// Messages start with self::PACKED_MSG_LEAD
|
||||
elseif ($x !== self::PACKED_MSG_LEAD)
|
||||
throw new InvalidPacketException('Not a valid packet: '.bin2hex($x));
|
||||
|
||||
// No message attached
|
||||
else if (! strlen($x))
|
||||
throw new InvalidPacketException('No message in packet: '.bin2hex($x));
|
||||
} else
|
||||
throw new InvalidPacketException('Not a valid packet, not EOP or SOM:'.bin2hex($x));
|
||||
|
||||
$o->zone = $system?->zones->firstWhere('zone_id',$o->fz);
|
||||
Log::info(sprintf('%s:- Packet [%s] is a [%s] packet, dated [%s]',self::LOGKEY,$o->name,get_class($o),$o->date));
|
||||
|
||||
// If zone is null, we'll take the zone from the packet
|
||||
if (! $o->zone)
|
||||
$o->zone = Zone::where('zone_id',$o->fz)->where('default',TRUE)->single();
|
||||
|
||||
$buf_ptr = 0;
|
||||
$message = '';
|
||||
$readbuf = '';
|
||||
$last = '';
|
||||
|
||||
while ($buf_ptr || (! feof($f) && ($readbuf=fread($f,self::BLOCKSIZE)))) {
|
||||
if (! $buf_ptr)
|
||||
$read_ptr = ftell($f);
|
||||
|
||||
// Packed messages are Message::HEADER_LEN, prefixed with self::PACKED_MSG_LEAD
|
||||
if (strlen($message) < (Message::HEADER_LEN+strlen(self::PACKED_MSG_LEAD))) {
|
||||
$addchars = (Message::HEADER_LEN+strlen(self::PACKED_MSG_LEAD))-strlen($message);
|
||||
$message .= substr($readbuf,$buf_ptr,$addchars);
|
||||
$buf_ptr += $addchars;
|
||||
|
||||
// If our buffer wasnt big enough...
|
||||
if ($buf_ptr >= strlen($readbuf)) {
|
||||
$buf_ptr = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Take 2 chars from the buffer and check if we have our end packet signature
|
||||
if ($last && ($buf_ptr === 0)) {
|
||||
$last .= substr($readbuf,0,2);
|
||||
|
||||
if (($end=strpos($last,"\x00".self::PACKED_MSG_LEAD,$buf_ptr)) !== FALSE) {
|
||||
$o->parseMessage(substr($message,0,$end-2));
|
||||
$last = '';
|
||||
$message = '';
|
||||
$buf_ptr = 1+$end;
|
||||
|
||||
// Loop to rebuild our header for the next message
|
||||
continue;
|
||||
}
|
||||
|
||||
$last = '';
|
||||
}
|
||||
|
||||
if (($end=strpos($readbuf,"\x00".self::PACKED_MSG_LEAD,$buf_ptr)) === FALSE) {
|
||||
// Just in case our packet break is at the end of the buffer
|
||||
$last = substr($readbuf,-2);
|
||||
|
||||
if ((str_contains($last,"\x00")) && ($size-$read_ptr > 2)) {
|
||||
$message .= substr($readbuf,$buf_ptr);
|
||||
$buf_ptr = 0;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$last = '';
|
||||
$end = strpos($readbuf,"\x00\x00\x00",$buf_ptr);
|
||||
}
|
||||
|
||||
// See if we have found the end of the packet, if not read more.
|
||||
if ($end === FALSE && ($read_ptr < $size)) {
|
||||
$message .= substr($readbuf,$buf_ptr);
|
||||
$buf_ptr = 0;
|
||||
|
||||
continue;
|
||||
|
||||
} else {
|
||||
$message .= substr($readbuf,$buf_ptr,$end-$buf_ptr);
|
||||
$buf_ptr = $end+3;
|
||||
|
||||
if ($buf_ptr >= strlen($readbuf))
|
||||
$buf_ptr = 0;
|
||||
}
|
||||
|
||||
// Look for the next message
|
||||
$o->parseMessage($message);
|
||||
$message = '';
|
||||
// Work out the packet zone
|
||||
if ($o->fz && ($o->fd || $domain)) {
|
||||
$o->zone = Zone::select('zones.*')
|
||||
->join('domains',['domains.id'=>'zones.domain_id'])
|
||||
->where('zone_id',$o->fz)
|
||||
->where('name',$o->fd ?: $domain->name)
|
||||
->single();
|
||||
}
|
||||
|
||||
// If our message is still set, then we have an unprocessed message
|
||||
if ($message)
|
||||
$o->parseMessage($message);
|
||||
// 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));
|
||||
|
||||
$o->zone = Zone::where('zone_id',$o->fz)
|
||||
->where('default',TRUE)
|
||||
->singleOrFail();
|
||||
}
|
||||
|
||||
$message = ''; // Current message we are building
|
||||
$msgbuf = '';
|
||||
$leader = Message::header_len()+strlen(self::PACKED_MSG_LEAD);
|
||||
|
||||
// We loop through reading from the buffer, to find our end of message tag
|
||||
while ((! feof($f) && ($readbuf=fread($f,$leader)))) {
|
||||
$read_ptr = ftell($f);
|
||||
$msgbuf .= $readbuf;
|
||||
|
||||
// See if we have our EOM/EOP marker
|
||||
if ((($end=strpos($msgbuf,"\x00".self::PACKED_MSG_LEAD,$leader)) !== FALSE)
|
||||
|| (($end=strpos($msgbuf,"\x00".self::PACKED_END,$leader)) !== FALSE))
|
||||
{
|
||||
// Parse our message
|
||||
$o->parseMessage(substr($msgbuf,0,$end));
|
||||
|
||||
$msgbuf = substr($msgbuf,$end+3);
|
||||
continue;
|
||||
|
||||
// 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)));;
|
||||
}
|
||||
|
||||
if ($msgbuf)
|
||||
throw new InvalidPacketException(sprintf('Unprocessed data in packet: %s|%s',get_class($o),hex_dump($msgbuf)));
|
||||
|
||||
return $o;
|
||||
}
|
||||
|
||||
/**
|
||||
* Location of the version
|
||||
*
|
||||
* @return int
|
||||
* @param string|null $header
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function version_offset(): int
|
||||
public function __construct(string $header=NULL)
|
||||
{
|
||||
return Arr::get(collect(static::HEADER)->get('type'),0);
|
||||
$this->messages = collect();
|
||||
$this->errors = collect();
|
||||
|
||||
if ($header)
|
||||
$this->header = unpack(self::unpackheader(static::HEADER),$header);
|
||||
}
|
||||
|
||||
public static function version_offset_len(): int
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __get($key)
|
||||
{
|
||||
return Arr::get(collect(static::HEADER)->get('type'),2);
|
||||
//Log::debug(sprintf('%s:/ Requesting key for Packet::class [%s]',self::LOGKEY,$key));
|
||||
|
||||
switch ($key) {
|
||||
// From Addresses
|
||||
case 'fz': return Arr::get($this->header,'ozone');
|
||||
case 'fn': return Arr::get($this->header,'onet');
|
||||
case 'ff': return Arr::get($this->header,'onode');
|
||||
case 'fp': return Arr::get($this->header,'opoint');
|
||||
case 'fd': return rtrim(Arr::get($this->header,'odomain',"\x00"));
|
||||
|
||||
// To Addresses
|
||||
case 'tz': return Arr::get($this->header,'dzone');
|
||||
case 'tn': return Arr::get($this->header,'dnet');
|
||||
case 'tf': return Arr::get($this->header,'dnode');
|
||||
case 'tp': return Arr::get($this->header,'dpoint');
|
||||
case 'td': return rtrim(Arr::get($this->header,'ddomain',"\x00"));
|
||||
|
||||
case 'date':
|
||||
return Carbon::create(
|
||||
Arr::get($this->header,'y'),
|
||||
Arr::get($this->header,'m')+1,
|
||||
Arr::get($this->header,'d'),
|
||||
Arr::get($this->header,'H'),
|
||||
Arr::get($this->header,'M'),
|
||||
Arr::get($this->header,'S')
|
||||
);
|
||||
|
||||
case 'password':
|
||||
return rtrim(Arr::get($this->header,$key),"\x00");
|
||||
|
||||
case 'fftn_t':
|
||||
case 'fftn':
|
||||
case 'tftn_t':
|
||||
case 'tftn':
|
||||
return parent::__get($key);
|
||||
|
||||
case 'product':
|
||||
return Arr::get($this->header,'prodcode-hi')<<8|Arr::get($this->header,'prodcode-lo');
|
||||
|
||||
case 'software':
|
||||
Software::unguard();
|
||||
$o = Software::singleOrNew(['code'=>$this->product,'type'=>Software::SOFTWARE_TOSSER]);
|
||||
Software::reguard();
|
||||
|
||||
return $o;
|
||||
|
||||
case 'software_ver':
|
||||
return sprintf('%d.%d',Arr::get($this->header,'prodrev-maj'),Arr::get($this->header,'prodrev-min'));
|
||||
|
||||
case 'capability':
|
||||
// This needs to be defined in child classes, since not all children have it
|
||||
return NULL;
|
||||
|
||||
// Packet Type
|
||||
case 'type':
|
||||
return static::TYPE;
|
||||
|
||||
// Packet name:
|
||||
case 'name':
|
||||
return $this->{$key} ?: sprintf('%08x',timew());
|
||||
|
||||
case 'messages':
|
||||
return $this->{$key};
|
||||
|
||||
default:
|
||||
throw new \Exception('Unknown key: '.$key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the packet
|
||||
*
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
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 */
|
||||
@ -336,14 +312,14 @@ class Packet extends FTNBase implements \Iterator, \Countable
|
||||
return $this->messages->count();
|
||||
}
|
||||
|
||||
public function current(): Message
|
||||
public function current(): Echomail|Netmail
|
||||
{
|
||||
return $this->use_cache ? unserialize(Cache::pull($this->key())) : $this->messages->get($this->index);
|
||||
return $this->messages->get($this->index);
|
||||
}
|
||||
|
||||
public function key(): mixed
|
||||
{
|
||||
return $this->use_cache ? $this->messages->get($this->index) : $this->index;
|
||||
return $this->index;
|
||||
}
|
||||
|
||||
public function next(): void
|
||||
@ -358,7 +334,7 @@ class Packet extends FTNBase implements \Iterator, \Countable
|
||||
|
||||
public function valid(): bool
|
||||
{
|
||||
return (! is_null($this->key())) && ($this->use_cache ? Cache::has($this->key()) : $this->messages->has($this->key()));
|
||||
return (! is_null($this->key())) && $this->messages->has($this->key());
|
||||
}
|
||||
|
||||
/* METHODS */
|
||||
@ -369,6 +345,7 @@ class Packet extends FTNBase implements \Iterator, \Countable
|
||||
* @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
|
||||
{
|
||||
@ -394,7 +371,7 @@ class Packet extends FTNBase implements \Iterator, \Countable
|
||||
'H' => $date->format('H'), // Hour
|
||||
'M' => $date->format('i'), // Minute
|
||||
'S' => $date->format('s'), // Second
|
||||
'password' => (! is_null($passwd)) ? $passwd : $o->session('pktpass'), // Packet Password
|
||||
'password' => strtoupper((! is_null($passwd)) ? $passwd : $o->session('pktpass')), // Packet Password
|
||||
];
|
||||
}
|
||||
|
||||
@ -402,12 +379,38 @@ class Packet extends FTNBase implements \Iterator, \Countable
|
||||
* 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;
|
||||
$this->fftn_p = our_address($ao);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a packet
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generate(): string
|
||||
{
|
||||
return (string)$this;
|
||||
}
|
||||
|
||||
public function mail(Collection $msgs): self
|
||||
{
|
||||
$this->messages = $msgs;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a message in a mail packet
|
||||
*
|
||||
@ -416,113 +419,100 @@ class Packet extends FTNBase implements \Iterator, \Countable
|
||||
*/
|
||||
private function parseMessage(string $message): void
|
||||
{
|
||||
Log::info(sprintf('%s:Processing message [%d] bytes',self::LOGKEY,strlen($message)));
|
||||
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 ($msg->errors) {
|
||||
Log::info(sprintf('%s:- Message [%s] has errors',self::LOGKEY,$msg->msgid));
|
||||
if ($msg->errors->count()) {
|
||||
Log::info(sprintf('%s:- Message [%s] has [%d] errors',self::LOGKEY,$msg->msgid ?: 'No ID',$msg->errors->count()));
|
||||
|
||||
// If the from address doenst exist, we'll create a new entry
|
||||
if ($msg->errors->messages()->has('to') && $msg->tzone) {
|
||||
try {
|
||||
// @todo Need to work out the correct region for the host_id
|
||||
Address::unguard();
|
||||
$ao = Address::firstOrNew([
|
||||
'zone_id' => $msg->tzone->id,
|
||||
//'region_id' => 0,
|
||||
'host_id' => $msg->tn,
|
||||
'node_id' => $msg->tf,
|
||||
'point_id' => $msg->tp,
|
||||
'active' => TRUE,
|
||||
]);
|
||||
Address::reguard();
|
||||
|
||||
if (is_null($ao->region_id))
|
||||
$ao->region_id = $ao->host_id;
|
||||
// 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 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(new EchomailBadAddress($msg));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If the $msg->fftn doesnt exist, we'll need to create it
|
||||
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);
|
||||
|
||||
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));
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error(sprintf('%s:! Error finding/creating TO address [%s] for message',self::LOGKEY,$msg->tboss),['error'=>$e->getMessage()]);
|
||||
$this->errors->push($msg);
|
||||
return;
|
||||
}
|
||||
|
||||
$ao->role = Address::NODE_UNKNOWN;
|
||||
if (! $ao) {
|
||||
$so = System::createUnknownSystem();
|
||||
$ao = Address::createFTN($msg->set->get('set_fftn'),$so);
|
||||
}
|
||||
|
||||
System::unguard();
|
||||
$so = System::firstOrCreate([
|
||||
'name' => 'Discovered System',
|
||||
'sysop' => 'Unknown',
|
||||
'location' => '',
|
||||
'active' => TRUE,
|
||||
]);
|
||||
System::reguard();
|
||||
if ($so->id !== 443)
|
||||
Log::alert(sprintf('%s:? Just created Discovered System for MSGID [%s] A',self::LOGKEY,$msg->msgid));
|
||||
|
||||
$so->addresses()->save($ao);
|
||||
|
||||
Log::alert(sprintf('%s: - To FTN is not defined, creating new entry for [%s] (%d)',self::LOGKEY,$msg->tboss,$ao->id));
|
||||
$msg->fftn_id = $ao->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 ($msg->errors->messages()->has('from') && $msg->tzone) {
|
||||
try {
|
||||
// @todo Need to work out the correct region for the host_id
|
||||
Address::unguard();
|
||||
$ao = Address::firstOrNew([
|
||||
'zone_id' => $msg->fzone->id,
|
||||
//'region_id' => 0,
|
||||
'host_id' => $msg->fn,
|
||||
'node_id' => $msg->ff,
|
||||
'point_id' => $msg->fp,
|
||||
'active'=> TRUE,
|
||||
]);
|
||||
Address::reguard();
|
||||
// If the $msg->tftn doesnt exist, we'll need to create it
|
||||
if ($msg->errors->has('to') && $this->tftn && $this->tftn->zone_id) {
|
||||
Log::debug(sprintf('%s:^ To address [%s] doesnt exist, it needs to be created',self::LOGKEY,$msg->set->get('set_tftn')));
|
||||
|
||||
if (is_null($ao->region_id))
|
||||
$ao->region_id = $ao->host_id;
|
||||
$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));
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error(sprintf('%s:! Error finding/creating FROM address [%s] for message',self::LOGKEY,$msg->fboss),['error'=>$e->getMessage()]);
|
||||
$this->errors->push($msg);
|
||||
return;
|
||||
}
|
||||
|
||||
$ao->role = Address::NODE_UNKNOWN;
|
||||
if (! $ao) {
|
||||
$so = System::createUnknownSystem();
|
||||
$ao = Address::createFTN($msg->set->get('set_fftn'),$so);
|
||||
}
|
||||
|
||||
System::unguard();
|
||||
$so = System::firstOrCreate([
|
||||
'name' => 'Discovered System',
|
||||
'sysop' => 'Unknown',
|
||||
'location' => '',
|
||||
'active' => TRUE,
|
||||
]);
|
||||
System::reguard();
|
||||
if ($so->id !== 443)
|
||||
Log::alert(sprintf('%s:? Just created Discovered System for MSGID [%s] B',self::LOGKEY,$msg->msgid));
|
||||
|
||||
$so->addresses()->save($ao);
|
||||
|
||||
Log::alert(sprintf('%s: - From FTN is not defined, creating new entry for [%s] (%d)',self::LOGKEY,$msg->fboss,$ao->id));
|
||||
$msg->tftn_id = $ao->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 ($msg->errors->messages()->has('user_from') || $msg->errors->messages()->has('user_to')) {
|
||||
Log::error(sprintf('%s:! Skipping message [%s] due to errors (%s)...',self::LOGKEY,$msg->msgid,join(',',$msg->errors->messages()->keys())));
|
||||
$this->errors->push($msg);
|
||||
// If there is no fftn, then its from a system that we dont know about
|
||||
if (! $this->fftn) {
|
||||
Log::alert(sprintf('%s:! No further message processing, packet is from a system we dont know about [%s]',self::LOGKEY,$this->fftn_t));
|
||||
|
||||
$this->messages->push($msg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->use_cache) {
|
||||
$key = urlencode($msg->msgid ?: sprintf('%s %s',$msg->fftn,Carbon::now()->timestamp));
|
||||
if (! Cache::forever($key,serialize($msg)))
|
||||
throw new \Exception(sprintf('Caching failed for key [%s]?',$key));
|
||||
// @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));
|
||||
|
||||
$this->messages->push($key);
|
||||
$this->messages->push($msg);
|
||||
}
|
||||
|
||||
} else {
|
||||
$this->messages->push($msg);
|
||||
}
|
||||
/**
|
||||
* Overwrite the packet password
|
||||
*
|
||||
* @param string|null $password
|
||||
* @return self
|
||||
*/
|
||||
public function password(string $password=NULL): self
|
||||
{
|
||||
if ($password && (strlen($password) < 9))
|
||||
$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);
|
||||
}
|
||||
}
|
@ -30,14 +30,14 @@ final class FSC39 extends Packet
|
||||
'dnet' => [0x16,'v',2], // Dest Net
|
||||
'prodcode-lo' => [0x18,'C',1], // Product Code
|
||||
'prodrev-maj' => [0x19,'C',1], // Product Version Major
|
||||
'password' => [0x1a,'a8',8], // Packet Password
|
||||
'password' => [0x1a,'a8',8], // Packet Password - http://ftsc.org/docs/fsc-0039.004 packet passwords are A-Z,0-9
|
||||
'ozone' => [0x22,'v',2], // Orig Zone
|
||||
'dzone' => [0x24,'v',2], // Dest Zone
|
||||
'reserved' => [0x26,'a2',2], // Reserved
|
||||
'capvalid' => [0x28,'n',2], // fsc-0039.004 (copy of 0x2c)
|
||||
'prodcode-hi' => [0x2a,'C',1], // Product Code Hi
|
||||
'prodrev-min' => [0x2b,'C',1], // Product Version Minor
|
||||
'capword' => [0x2c,'v',2], // Capability Word
|
||||
'capword' => [0x2c,'v',2], // Capability Word fsc-0039.004/fsc-0048.002
|
||||
'dozone' => [0x2e,'v',2], // Orig Zone
|
||||
'ddzone' => [0x30,'v',2], // Dest Zone
|
||||
'opoint' => [0x32,'v',2], // Orig Point
|
||||
@ -46,6 +46,7 @@ final class FSC39 extends Packet
|
||||
];
|
||||
|
||||
public const TYPE = '2e';
|
||||
public const VERS = self::MSG_TYPE2; //|self::MSG_TYPE4;
|
||||
|
||||
public function __get($key)
|
||||
{
|
||||
@ -63,34 +64,36 @@ final class FSC39 extends Packet
|
||||
*/
|
||||
protected function header(): string
|
||||
{
|
||||
$oldest = $this->messages->sortBy('datetime')->last();
|
||||
|
||||
try {
|
||||
return pack(collect(self::HEADER)->pluck(1)->join(''),
|
||||
$this->ff, // Orig Node
|
||||
$this->tf, // Dest Node
|
||||
Arr::get($this->header,'y'), // Year
|
||||
Arr::get($this->header,'m'), // Month
|
||||
Arr::get($this->header,'d'), // Day
|
||||
Arr::get($this->header,'H'), // Hour
|
||||
Arr::get($this->header,'M'), // Minute
|
||||
Arr::get($this->header,'S'), // Second
|
||||
0, // Baud
|
||||
2, // Packet Version (should be 2)
|
||||
$this->fn, // Orig Net
|
||||
$this->tn, // Dest Net
|
||||
(Setup::PRODUCT_ID & 0xff), // Product Code Lo
|
||||
Setup::PRODUCT_VERSION_MAJ, // Product Version Major
|
||||
$this->password, // Packet Password
|
||||
$this->fz, // Orig Zone
|
||||
$this->tz, // Dest Zone
|
||||
'', // Reserved
|
||||
Arr::get($this->header,'capvalid',1<<0), // fsc-0039.004 (copy of 0x2c)
|
||||
((Setup::PRODUCT_ID >> 8) & 0xff), // Product Code Hi
|
||||
Setup::PRODUCT_VERSION_MIN, // Product Version Minor
|
||||
Arr::get($this->header,'capword',1<<0), // Capability Word
|
||||
$this->fz, // Orig Zone
|
||||
$this->tz, // Dest Zone
|
||||
$this->fp, // Orig Point
|
||||
$this->tp, // Dest Point
|
||||
$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
|
||||
);
|
||||
|
||||
|
@ -37,6 +37,20 @@ final class FSC45 extends Packet
|
||||
|
||||
public const TYPE = '2.2';
|
||||
|
||||
public function __get($key)
|
||||
{
|
||||
switch ($key) {
|
||||
case 'product':
|
||||
return Arr::get($this->header,'prodcode');
|
||||
|
||||
case 'software_ver':
|
||||
return Arr::get($this->header,'prodrev-maj');
|
||||
|
||||
default:
|
||||
return parent::__get($key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create our message packet header
|
||||
*/
|
||||
@ -44,22 +58,22 @@ final class FSC45 extends Packet
|
||||
{
|
||||
try {
|
||||
return pack(collect(self::HEADER)->pluck(1)->join(''),
|
||||
$this->ff, // Orig Node
|
||||
$this->tf, // Dest Node
|
||||
$this->fp, // Orig Point
|
||||
$this->tp, // Dest Point
|
||||
'', // Reserved
|
||||
2, // Sub Version (should be 2)
|
||||
2, // Packet Version (should be 2)
|
||||
$this->fn, // Orig Net
|
||||
$this->tn, // Dest Net
|
||||
(Setup::PRODUCT_ID & 0xff), // Product Code
|
||||
Setup::PRODUCT_VERSION_MAJ, // Product Version
|
||||
$this->password, // Packet Password
|
||||
$this->fz, // Orig Zone
|
||||
$this->tz, // Dest Zone
|
||||
$this->fd, // Orig Domain
|
||||
$this->td, // Dest Domain
|
||||
$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
|
||||
);
|
||||
|
||||
|
@ -11,6 +11,7 @@ use App\Models\Setup;
|
||||
* FSC-0048 http://ftsc.org/docs/fsc-0048.002
|
||||
*
|
||||
* Commonly known as Type 2+ packets, based on FSC-0039 with improved support for FTS-0001
|
||||
* @note: These packets will be detected as FSC-0039 packets unless it is addressed to a point
|
||||
*/
|
||||
final class FSC48 extends Packet
|
||||
{
|
||||
@ -36,7 +37,7 @@ final class FSC48 extends Packet
|
||||
'capvalid' => [0x28,'n',2], // fsc-0039.004 (copy of 0x2c)
|
||||
'prodcode-hi' => [0x2a,'C',1], // Product Code Hi
|
||||
'prodrev-min' => [0x2b,'C',1], // Product Version Minor
|
||||
'capword' => [0x2c,'v',2], // Capability Word
|
||||
'capword' => [0x2c,'v',2], // Capability Word fsc-0039.004/fsc-0048.002
|
||||
'dozone' => [0x2e,'v',2], // Orig Zone
|
||||
'ddzone' => [0x30,'v',2], // Dest Zone
|
||||
'opoint' => [0x32,'v',2], // Orig Point
|
||||
@ -45,6 +46,7 @@ final class FSC48 extends Packet
|
||||
];
|
||||
|
||||
public const TYPE = '2+';
|
||||
public const VERS = self::MSG_TYPE2; //|self::MSG_TYPE4;
|
||||
|
||||
public function __get($key)
|
||||
{
|
||||
@ -62,34 +64,36 @@ final class FSC48 extends Packet
|
||||
*/
|
||||
protected function header(): string
|
||||
{
|
||||
$oldest = $this->messages->sortBy('datetime')->last();
|
||||
|
||||
try {
|
||||
return pack(collect(self::HEADER)->pluck(1)->join(''),
|
||||
$this->ff, // Orig Node
|
||||
$this->tf, // Dest Node
|
||||
Arr::get($this->header,'y'), // Year
|
||||
Arr::get($this->header,'m'), // Month
|
||||
Arr::get($this->header,'d'), // Day
|
||||
Arr::get($this->header,'H'), // Hour
|
||||
Arr::get($this->header,'M'), // Minute
|
||||
Arr::get($this->header,'S'), // Second
|
||||
0, // Baud
|
||||
2, // Packet Version (should be 2)
|
||||
$this->fp ? 0xffff : $this->fn, // Orig Net (0xFFFF when OrigPoint != 0)
|
||||
$this->tn, // Dest Net
|
||||
(Setup::PRODUCT_ID & 0xff), // Product Code Lo
|
||||
Setup::PRODUCT_VERSION_MAJ, // Product Version Major
|
||||
$this->password, // Packet Password
|
||||
$this->fz, // Orig Zone
|
||||
$this->tz, // Dest Zone
|
||||
$this->fp ? $this->fn : 0x00, // Aux Net
|
||||
Arr::get($this->header,'capvalid',1<<0), // fsc-0039.004 (copy of 0x2c)
|
||||
((Setup::PRODUCT_ID >> 8) & 0xff), // Product Code Hi
|
||||
Setup::PRODUCT_VERSION_MIN, // Product Version Minor
|
||||
Arr::get($this->header,'capword',1<<0), // Capability Word
|
||||
$this->fz, // Orig Zone
|
||||
$this->tz, // Dest Zone
|
||||
$this->fp, // Orig Point
|
||||
$this->tp, // Dest Point
|
||||
$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
|
||||
);
|
||||
|
||||
|
@ -38,31 +38,47 @@ final class FTS1 extends Packet
|
||||
|
||||
public const TYPE = '2';
|
||||
|
||||
public function __get($key)
|
||||
{
|
||||
switch ($key) {
|
||||
case 'product':
|
||||
return Arr::get($this->header,'prodcode');
|
||||
|
||||
case 'software_ver':
|
||||
return 'N/A';
|
||||
|
||||
default:
|
||||
return parent::__get($key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create our message packet header
|
||||
*/
|
||||
protected function header(): string
|
||||
{
|
||||
$oldest = $this->messages->sortBy('datetime')->last();
|
||||
|
||||
try {
|
||||
return pack(collect(self::HEADER)->pluck(1)->join(''),
|
||||
$this->ff, // Orig Node
|
||||
$this->tf, // Dest Node
|
||||
Arr::get($this->header,'y'), // Year
|
||||
Arr::get($this->header,'m'), // Month
|
||||
Arr::get($this->header,'d'), // Day
|
||||
Arr::get($this->header,'H'), // Hour
|
||||
Arr::get($this->header,'M'), // Minute
|
||||
Arr::get($this->header,'S'), // Second
|
||||
0, // Baud
|
||||
2, // Packet Version (should be 2)
|
||||
$this->fn, // Orig Net
|
||||
$this->tn, // Dest Net
|
||||
(Setup::PRODUCT_ID & 0xff), // Product Code Lo
|
||||
Setup::PRODUCT_VERSION_MAJ, // Product Version Major
|
||||
$this->password, // Packet Password
|
||||
$this->fz, // Orig Zone
|
||||
$this->tz, // Dest Zone
|
||||
'', // Reserved
|
||||
$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) {
|
||||
|
@ -2,25 +2,23 @@
|
||||
|
||||
namespace App\Classes\FTN;
|
||||
|
||||
use App\Models\Echoarea;
|
||||
use App\Models\{Echoarea,Echomail,Netmail};
|
||||
|
||||
/**
|
||||
* Abstract class to hold the common functions for automatic responding to echomail/netmail messages
|
||||
*/
|
||||
abstract class Process
|
||||
{
|
||||
public static function canProcess(string $echoarea): bool
|
||||
public static function canProcess(Echoarea $eao): bool
|
||||
{
|
||||
$eao = Echoarea::where('name',$echoarea)->single();
|
||||
|
||||
return $eao && $eao->automsgs;
|
||||
return $eao->automsgs ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return TRUE if the process class handled the message.
|
||||
*
|
||||
* @param Message $msg
|
||||
* @param Echomail|Netmail $mo
|
||||
* @return bool
|
||||
*/
|
||||
abstract public static function handle(Message $msg): bool;
|
||||
abstract public static function handle(Echomail|Netmail $mo): bool;
|
||||
}
|
@ -5,7 +5,8 @@ namespace App\Classes\FTN\Process\Echomail;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
use App\Classes\FTN\{Message,Process};
|
||||
use App\Classes\FTN\Process;
|
||||
use App\Models\{Echomail,Netmail};
|
||||
use App\Notifications\Echomails\Test as TestNotification;
|
||||
|
||||
/**
|
||||
@ -19,16 +20,16 @@ final class Test extends Process
|
||||
|
||||
private const testing = ['test','testing'];
|
||||
|
||||
public static function handle(Message $msg): bool
|
||||
public static function handle(Echomail|Netmail $mo): bool
|
||||
{
|
||||
if (! self::canProcess($msg->echoarea)
|
||||
|| (strtolower($msg->user_to) !== 'all')
|
||||
|| (! in_array(strtolower($msg->subject),self::testing)))
|
||||
if (! self::canProcess($mo->echoarea)
|
||||
|| (strtolower($mo->to) !== 'all')
|
||||
|| (! in_array(strtolower($mo->subject),self::testing)))
|
||||
return FALSE;
|
||||
|
||||
Log::info(sprintf('%s:- Processing TEST message from (%s) [%s] in [%s]',self::LOGKEY,$msg->user_from,$msg->fftn,$msg->echoarea));
|
||||
Log::info(sprintf('%s:- Processing TEST message from (%s) [%s] in [%s]',self::LOGKEY,$mo->from,$mo->fftn->ftn,$mo->echoarea->name));
|
||||
|
||||
Notification::route('echomail',$msg->echoarea)->notify(new TestNotification($msg));
|
||||
Notification::route('echomail',$mo->echoarea->withoutRelations())->notify(new TestNotification($mo));
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
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;
|
||||
}
|
||||
}
|
@ -5,7 +5,8 @@ namespace App\Classes\FTN\Process\Netmail;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
use App\Classes\FTN\{Message,Process};
|
||||
use App\Classes\FTN\Process;
|
||||
use App\Models\{Echomail,Netmail};
|
||||
use App\Notifications\Netmails\Ping as PingNotification;
|
||||
|
||||
/**
|
||||
@ -17,14 +18,14 @@ final class Ping extends Process
|
||||
{
|
||||
private const LOGKEY = 'RP-';
|
||||
|
||||
public static function handle(Message $msg): bool
|
||||
public static function handle(Echomail|Netmail $mo): bool
|
||||
{
|
||||
if (strtolower($msg->user_to) !== 'ping')
|
||||
if (strtolower($mo->to) !== 'ping')
|
||||
return FALSE;
|
||||
|
||||
Log::info(sprintf('%s:- Processing PING message from (%s) [%s]',self::LOGKEY,$msg->user_from,$msg->fftn));
|
||||
Log::info(sprintf('%s:- Processing PING message from (%s) [%s]',self::LOGKEY,$mo->from,$mo->fftn->ftn));
|
||||
|
||||
Notification::route('netmail',$msg->fftn_o)->notify(new PingNotification($msg));
|
||||
Notification::route('netmail',$mo->fftn)->notify(new PingNotification($mo));
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
@ -4,30 +4,30 @@ namespace App\Classes\FTN;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use League\Flysystem\UnableToWriteFile;
|
||||
use League\Flysystem\UnableToReadFile;
|
||||
|
||||
use App\Classes\FTN as FTNBase;
|
||||
use App\Exceptions\{InvalidCRCException,
|
||||
InvalidPasswordException,
|
||||
NodeNotSubscribedException,
|
||||
NoWriteSecurityException};
|
||||
use App\Exceptions\TIC\{NoFileAreaException,NotToMeException,SizeMismatchException};
|
||||
use App\Models\{Address,File,Filearea,Setup};
|
||||
use App\Traits\EncodeUTF8;
|
||||
|
||||
/**
|
||||
* Class TIC
|
||||
* Used create the structure of TIC files
|
||||
* This class handles the TIC files that accompany file transfers
|
||||
*
|
||||
* @package App\Classes
|
||||
*/
|
||||
class Tic extends FTNBase
|
||||
{
|
||||
use EncodeUTF8;
|
||||
|
||||
private const LOGKEY = 'FT-';
|
||||
|
||||
private const cast_utf8 = [
|
||||
];
|
||||
|
||||
// Single value kludge items and whether they are required
|
||||
// http://ftsc.org/docs/fts-5006.001
|
||||
private array $_kludge = [
|
||||
@ -52,75 +52,79 @@ class Tic extends FTNBase
|
||||
'pw' => FALSE, // Password
|
||||
];
|
||||
|
||||
private File $fo;
|
||||
private Filearea $area;
|
||||
private Collection $values;
|
||||
|
||||
private Address $origin; // Should be first address in Path
|
||||
private Address $from; // Should be last address in Path
|
||||
private File $file;
|
||||
private Address $to; // Should be me
|
||||
|
||||
public function __construct()
|
||||
public function __construct(File $file=NULL)
|
||||
{
|
||||
$this->fo = new File;
|
||||
$this->file = $file ?: new File;
|
||||
|
||||
$this->fo->kludges = collect();
|
||||
$this->fo->set_path = collect();
|
||||
$this->fo->set_seenby = collect();
|
||||
$this->fo->rogue_path = collect();
|
||||
$this->fo->rogue_seenby = collect();
|
||||
|
||||
$this->values = collect();
|
||||
$this->file->kludges = collect();
|
||||
$this->file->rogue_seenby = collect();
|
||||
$this->file->set_path = collect();
|
||||
$this->file->set_seenby = collect();
|
||||
}
|
||||
|
||||
public function __get(string $key): mixed
|
||||
{
|
||||
switch ($key) {
|
||||
case 'fo':
|
||||
case 'file':
|
||||
return $this->{$key};
|
||||
|
||||
case 'name':
|
||||
return $this->file->name;
|
||||
|
||||
default:
|
||||
return parent::__get($key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a TIC file for an address
|
||||
* Generate the TIC file
|
||||
*
|
||||
* @param Address $ao
|
||||
* @param File $fo
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function generate(Address $ao,File $fo): string
|
||||
public function __toString(): string
|
||||
{
|
||||
$sysaddress = Setup::findOrFail(config('app.id'))->system->match($ao->zone)->first();
|
||||
if (! $this->to)
|
||||
throw new \Exception('No to address defined');
|
||||
|
||||
$sysaddress = our_address($this->to);
|
||||
|
||||
$result = collect();
|
||||
|
||||
// Origin is the first address in our path
|
||||
$result->put('ORIGIN',$fo->path->first()->ftn3d);
|
||||
$result->put('ORIGIN',$this->file->path->first()->ftn3d);
|
||||
$result->put('FROM',$sysaddress->ftn3d);
|
||||
$result->put('TO',$ao->ftn3d);
|
||||
$result->put('FILE',$fo->name);
|
||||
$result->put('SIZE',$fo->size);
|
||||
if ($fo->description)
|
||||
$result->put('DESC',$fo->description);
|
||||
$result->put('AREA',$fo->filearea->name);
|
||||
$result->put('AREADESC',$fo->filearea->description);
|
||||
if ($x=$ao->session('ticpass'))
|
||||
$result->put('TO',$this->to->ftn3d);
|
||||
$result->put('FILE',$this->file->name);
|
||||
$result->put('SIZE',$this->file->size);
|
||||
if ($this->file->description)
|
||||
$result->put('DESC',$this->file->description);
|
||||
if ($this->file->replaces)
|
||||
$result->put('REPLACES',$this->file->replaces);
|
||||
$result->put('AREA',$this->file->filearea->name);
|
||||
$result->put('AREADESC',$this->file->filearea->description);
|
||||
if ($x=strtoupper($this->to->session('ticpass')))
|
||||
$result->put('PW',$x);
|
||||
$result->put('CRC',sprintf("%X",$fo->crc));
|
||||
$result->put('CRC',sprintf("%X",$this->file->crc));
|
||||
|
||||
$out = '';
|
||||
foreach ($result as $key=>$value)
|
||||
$out .= sprintf("%s %s\r\n",$key,$value);
|
||||
|
||||
foreach ($fo->path as $o)
|
||||
foreach ($this->file->path as $o)
|
||||
$out .= sprintf("PATH %s %s %s\r\n",$o->ftn3d,$o->pivot->datetime,$o->pivot->extra);
|
||||
|
||||
foreach ($fo->seenby as $o)
|
||||
// Add ourself to the path:
|
||||
$out .= sprintf("PATH %s %s\r\n",$sysaddress->ftn3d,Carbon::now());
|
||||
|
||||
foreach ($this->file->seenby as $o)
|
||||
$out .= sprintf("SEENBY %s\r\n",$o->ftn3d);
|
||||
|
||||
$out .= sprintf("SEENBY %s\r\n",$sysaddress->ftn3d);
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
@ -131,180 +135,253 @@ class Tic extends FTNBase
|
||||
*/
|
||||
public function isNodelist(): bool
|
||||
{
|
||||
return (($this->fo->nodelist_filearea_id === $this->fo->filearea->domain->filearea_id)
|
||||
&& (preg_match(str_replace(['.','?'],['\.','.'],'#^'.$this->fo->filearea->domain->nodelist_filename.'$#i'),$this->fo->name)));
|
||||
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,
|
||||
str_replace(['.','?'],['\.','[0-9]'],'#^'.$this->file->filearea->domain->nodelist_filename.'$#i'),
|
||||
$this->file->name,
|
||||
));
|
||||
return (($this->file->nodelist_filearea_id === $this->file->filearea->domain->filearea_id)
|
||||
&& (preg_match(str_replace(['.','?'],['\.','[0-9]'],'#^'.$this->file->filearea->domain->nodelist_filename.'$#i'),$this->file->name)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a TIC file from an existing filename
|
||||
*
|
||||
* @param string $filename
|
||||
* @return void
|
||||
* @param string $filename Relative to filesystem
|
||||
* @return File
|
||||
* @throws FileNotFoundException
|
||||
* @throws InvalidCRCException
|
||||
* @throws InvalidPasswordException
|
||||
* @throws NoFileAreaException
|
||||
* @throws NoWriteSecurityException
|
||||
* @throws NodeNotSubscribedException
|
||||
* @throws NotToMeException
|
||||
* @throws SizeMismatchException
|
||||
*/
|
||||
public function load(string $filename): void
|
||||
public function load(string $filename): File
|
||||
{
|
||||
Log::info(sprintf('%s:+ Processing TIC file [%s]',self::LOGKEY,$filename));
|
||||
$fs = Storage::disk(config('fido.local_disk'));
|
||||
$rel_path_name = sprintf('%s/%s',config('fido.dir'),$filename);
|
||||
|
||||
if (str_contains($filename,'-')) {
|
||||
list($hex,$name) = explode('-',$filename);
|
||||
$hex = basename($hex);
|
||||
} else {
|
||||
$hex = '';
|
||||
if (! $fs->exists($rel_path_name))
|
||||
throw new FileNotFoundException(sprintf('File [%s] doesnt exist',$fs->path($rel_path_name)));
|
||||
|
||||
if ((! is_readable($fs->path($rel_path_name))) || ! ($f = $fs->readStream($rel_path_name)))
|
||||
throw new UnableToReadFile(sprintf('File [%s] is not readable',$fs->path($rel_path_name)));
|
||||
|
||||
/*
|
||||
* Filenames are in the format X-Y-N.tic
|
||||
* Where:
|
||||
* - X is the nodes address that sent us the file
|
||||
* - Y is the mtime of the TIC file from the sender
|
||||
* - N is the sender's filename
|
||||
*/
|
||||
|
||||
$aid = NULL;
|
||||
$mtime = NULL;
|
||||
$this->file->recv_tic = preg_replace('/\.[Tt][Ii][Cc]$/','',$filename);
|
||||
|
||||
$m = [];
|
||||
if (preg_match(sprintf('/^%s\.[Tt][Ii][Cc]$/',Packet::regex),$filename,$m)) {
|
||||
$aid = $m[1];
|
||||
$mtime = $m[2];
|
||||
$this->file->recv_tic = $m[3];
|
||||
}
|
||||
|
||||
if (! file_exists($filename))
|
||||
throw new FileNotFoundException(sprintf('File [%s] doesnt exist',$filename));
|
||||
|
||||
if (! is_readable($filename))
|
||||
throw new UnableToWriteFile(sprintf('File [%s] is not readable',realpath($filename)));
|
||||
|
||||
$f = fopen($filename,'rb');
|
||||
if (! $f) {
|
||||
Log::error(sprintf('%s:! Unable to open file [%s] for writing',self::LOGKEY,$filename));
|
||||
return;
|
||||
}
|
||||
$ldesc = '';
|
||||
|
||||
while (! feof($f)) {
|
||||
$line = chop(fgets($f));
|
||||
$matches = [];
|
||||
$m = [];
|
||||
|
||||
if (! $line)
|
||||
continue;
|
||||
|
||||
preg_match('/([a-zA-Z]+)\ (.*)/',$line,$matches);
|
||||
preg_match('/([a-zA-Z]+)\ ?(.*)?/',$line,$m);
|
||||
|
||||
if (in_array(strtolower($matches[1]),$this->_kludge)) {
|
||||
switch ($k=strtolower($matches[1])) {
|
||||
if (in_array(strtolower(Arr::get($m,1,'-')),$this->_kludge)) {
|
||||
switch ($k=strtolower($m[1])) {
|
||||
case 'area':
|
||||
$this->{$k} = Filearea::singleOrNew(['name'=>strtoupper($matches[2])]);
|
||||
try {
|
||||
if ($fo=Filearea::where('name',strtoupper($m[2]))->firstOrFail())
|
||||
$this->file->filearea_id = $fo->id;
|
||||
|
||||
} catch (ModelNotFoundException $e) {
|
||||
// Rethrow this as No File Area
|
||||
throw new NoFileAreaException($e->getMessage());
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'origin':
|
||||
case 'from':
|
||||
case 'to':
|
||||
$this->{$k} = Address::findFTN($matches[2]);
|
||||
if (($ao=Address::findFTN($m[2])) && ((! $aid) || ($ao->zone->domain_id === Address::findOrFail(hexdec($aid))->zone->domain_id)))
|
||||
$this->file->fftn_id = $ao->id;
|
||||
else
|
||||
throw new ModelNotFoundException(sprintf('FTN Address [%s] not found or sender mismatch',$m[2]));
|
||||
|
||||
break;
|
||||
|
||||
// The origin should be the first address in the path
|
||||
case 'origin':
|
||||
// Ignore
|
||||
case 'areadesc':
|
||||
case 'created':
|
||||
break;
|
||||
|
||||
// This should be one of my addresses
|
||||
case 'to':
|
||||
$ftns = our_address()->pluck('ftn3d');
|
||||
|
||||
if (! ($ftns->contains($m[2])))
|
||||
throw new NotToMeException(sprintf('FTN Address [%s] not found or not one of my addresses',$m[2]));
|
||||
|
||||
// @todo If $this->{$k} is null, we have discovered the system and it should be created
|
||||
break;
|
||||
|
||||
case 'file':
|
||||
if (! Storage::disk('local')->exists($x=sprintf('%s/%s-%s',config('app.fido'),$hex,$matches[2])))
|
||||
throw new FileNotFoundException(sprintf('File not found? [%s]',$x));
|
||||
$this->file->name = $m[2];
|
||||
|
||||
$this->fo->name = $matches[2];
|
||||
$this->fo->fullname = $x;
|
||||
break;
|
||||
|
||||
case 'areadesc':
|
||||
$areadesc = $matches[2];
|
||||
break;
|
||||
|
||||
case 'created':
|
||||
// ignored
|
||||
break;
|
||||
|
||||
case 'pw':
|
||||
$pw = $matches[2];
|
||||
$pw = $m[2];
|
||||
|
||||
break;
|
||||
|
||||
case 'lfile':
|
||||
$this->fo->lname = $matches[2];
|
||||
case 'fullname':
|
||||
$this->file->lname = $m[2];
|
||||
|
||||
break;
|
||||
|
||||
case 'desc':
|
||||
case 'magic':
|
||||
case 'replaces':
|
||||
case 'size':
|
||||
$this->fo->{$k} = $matches[2];
|
||||
break;
|
||||
$this->file->{$k} = $m[2];
|
||||
|
||||
case 'fullname':
|
||||
$this->fo->lfile = $matches[2];
|
||||
break;
|
||||
|
||||
case 'date':
|
||||
$this->fo->datetime = Carbon::create($matches[2]);
|
||||
$this->file->datetime = Carbon::createFromTimestamp($m[2]);
|
||||
|
||||
break;
|
||||
|
||||
case 'ldesc':
|
||||
$this->fo->{$k} .= $matches[2];
|
||||
$ldesc .= ($ldesc ? "\r" : '').$m[2];
|
||||
|
||||
break;
|
||||
|
||||
case 'crc':
|
||||
$this->fo->{$k} = hexdec($matches[2]);
|
||||
$this->file->{$k} = hexdec($m[2]);
|
||||
|
||||
break;
|
||||
|
||||
case 'path':
|
||||
$x = [];
|
||||
preg_match(sprintf('#^[Pp]ath (%s)\ ?([0-9]+)\ ?(.*)$#',Address::ftn_regex),$line,$x);
|
||||
$ao = Address::findFTN($x[1]);
|
||||
|
||||
if (! $ao) {
|
||||
$this->fo->rogue_path->push($matches[2]);
|
||||
} else {
|
||||
$this->fo->set_path->push(['address'=>$ao,'datetime'=>Carbon::createFromTimestamp($x[8]),'extra'=>$x[9]]);
|
||||
}
|
||||
$this->file->set_path->push($m[2]);
|
||||
|
||||
break;
|
||||
|
||||
case 'seenby':
|
||||
$ao = Address::findFTN($matches[2]);
|
||||
|
||||
if (! $ao) {
|
||||
$this->fo->rogue_seenby->push($matches[2]);
|
||||
} else {
|
||||
$this->fo->set_seenby->push($ao->id);
|
||||
}
|
||||
$this->file->set_seenby->push($m[2]);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
} else {
|
||||
$this->fo->kludges->push($line);
|
||||
$this->file->kludges->push($line);
|
||||
}
|
||||
}
|
||||
|
||||
fclose($f);
|
||||
|
||||
$f = fopen($x=Storage::disk('local')->path($this->fo->fullname),'rb');
|
||||
$stat = fstat($f);
|
||||
fclose($f);
|
||||
if ($ldesc)
|
||||
$this->file->ldesc = $ldesc;
|
||||
|
||||
// @todo Add notifictions back to the system
|
||||
// Validate Size
|
||||
if ($this->fo->size !== ($y=$stat['size']))
|
||||
throw new \Exception(sprintf('TIC file size [%d] doesnt match file [%s] (%d)',$this->fo->size,$this->fo->fullname,$y));
|
||||
// @todo Check that origin is the first address in the path
|
||||
// @todo Make sure origin/from are in seenby
|
||||
// @todo Make sure origin/from are in the path
|
||||
|
||||
// Validate CRC
|
||||
if (sprintf('%08x',$this->fo->crc) !== ($y=hash_file('crc32b',$x)))
|
||||
throw new \Exception(sprintf('TIC file CRC [%08x] doesnt match file [%s] (%s)',$this->fo->crc,$this->fo->fullname,$y));
|
||||
/*
|
||||
* Find our file and check the CRC
|
||||
* If there is more than 1 file, select files that within 24hrs of the TIC file.
|
||||
* If no files report file not found
|
||||
* If there is more than 1 check each CRC to match the right one.
|
||||
* If none match report, CRC error
|
||||
*/
|
||||
$found = FALSE;
|
||||
$crcOK = FALSE;
|
||||
foreach ($fs->files(config('fido.dir')) as $file) {
|
||||
if (abs($x=$fs->lastModified($rel_path_name)-$fs->lastModified($file)) > 86400) {
|
||||
Log::debug(sprintf('%s:/ Ignoring [%s] its mtime is outside of our scope [%d]',self::LOGKEY,$file,$x));
|
||||
|
||||
// Validate Password
|
||||
if ($pw !== ($y=$this->from->session('ticpass')))
|
||||
throw new \Exception(sprintf('TIC file PASSWORD [%s] doesnt match system [%s] (%s)',$pw,$this->from->ftn,$y));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate Sender is linked (and permitted to send)
|
||||
if ($this->from->fileareas->search(function($item) { return $item->id === $this->area->id; }) === FALSE)
|
||||
throw new \Exception(sprintf('Node [%s] is not subscribed to [%s]',$this->from->ftn,$this->area->name));
|
||||
// Our file should have the same prefix as the TIC file
|
||||
if (preg_match('#/'.($aid ? $aid.'-' : '').'.*'.$this->file->name.'$#',$file)) {
|
||||
$found = TRUE;
|
||||
|
||||
// If the filearea is to be autocreated, create it
|
||||
if (! $this->area->exists) {
|
||||
$this->area->description = $areadesc;
|
||||
$this->area->active = TRUE;
|
||||
$this->area->show = FALSE;
|
||||
$this->area->notes = 'Autocreated';
|
||||
$this->area->domain_id = $this->from->zone->domain_id;
|
||||
$this->area->save();
|
||||
if (sprintf('%08x',$this->file->crc) === ($y=$fs->checksum($file,['checksum_algo'=>'crc32b']))) {
|
||||
$crcOK = TRUE;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->fo->filearea_id = $this->area->id;
|
||||
$this->fo->fftn_id = $this->origin->id;
|
||||
if (($found) && (! $crcOK))
|
||||
throw new InvalidCRCException(sprintf('TIC file CRC [%08x] doesnt match file [%s] (%s)',$this->file->crc,$fs->path($rel_path_name),$y));
|
||||
elseif (! $found)
|
||||
throw new FileNotFoundException(sprintf('File not found? [%s...%s] in [%s]',$aid,$this->file->name,$fs->path($rel_path_name)));
|
||||
|
||||
// @todo Add notifications back to the system if the replaces line doesnt match
|
||||
if ($this->file->replaces && (! preg_match('/^'.$this->file->replaces.'$/',$this->file->name))) {
|
||||
Log::alert(sprintf('%s:! Regex [%s] doesnt match file name [%s]',self::LOGKEY,$this->file->replaces,$this->file->name));
|
||||
|
||||
$this->file->replaces = NULL;
|
||||
}
|
||||
|
||||
// @todo Add notification back to the system if no replaces line and the file already exists
|
||||
|
||||
// Validate Size
|
||||
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=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
|
||||
if ($this->file->fftn->fileareas->search(function($item) { return $item->id === $this->file->filearea_id; }) === FALSE)
|
||||
throw new NodeNotSubscribedException(sprintf('Node [%s] is not subscribed to [%s]',$this->file->fftn->ftn,$this->file->filearea->name));
|
||||
|
||||
// Validate sender is permitted to write
|
||||
// @todo Send a notification
|
||||
if (! $this->file->filearea->can_write($this->file->fftn->security))
|
||||
throw new NoWriteSecurityException(sprintf('Node [%s] doesnt have enough security to write to [%s] (%d)',$this->file->fftn->ftn,$this->file->filearea->name,$this->file->fftn->security));
|
||||
|
||||
// If the file create time is blank, we'll take the files
|
||||
if (! $this->fo->datetime)
|
||||
$this->fo->datetime = Carbon::createFromTimestamp($stat['ctime']);
|
||||
if (! $this->file->datetime)
|
||||
$this->file->datetime = Carbon::createFromTimestamp($fs->lastModified($file));
|
||||
|
||||
$this->fo->save();
|
||||
$this->file->src_file = $file;
|
||||
$this->file->recv_tic = $filename;
|
||||
|
||||
return $this->file;
|
||||
}
|
||||
|
||||
public function save(): bool
|
||||
{
|
||||
return $this->file->save();
|
||||
}
|
||||
|
||||
public function to(Address $ao): self
|
||||
{
|
||||
$this->to = $ao;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -7,6 +7,8 @@ use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\HttpFoundation\File\File as FileBase;
|
||||
|
||||
use App\Classes\FTN\Packet;
|
||||
|
||||
class File extends FileBase implements \Iterator
|
||||
{
|
||||
private const LOGKEY = 'F--';
|
||||
@ -102,13 +104,13 @@ class File extends FileBase implements \Iterator
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the name of the file, without a node ID prefix
|
||||
* Return the name of the file, without a node ID, mtime prefix
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function rawName(): string
|
||||
{
|
||||
return preg_replace('/^[0-9A-F]{4}-/','',$this->getFilename());
|
||||
return preg_replace(sprintf('/^%s\.pkt$/i',Packet::regex),'\3\4',$this->getFilename());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -129,7 +131,7 @@ class File extends FileBase implements \Iterator
|
||||
return preg_replace('/.pkt$/i','',Arr::get(stream_get_meta_data($f),'uri'));
|
||||
|
||||
} else {
|
||||
return $this->isPacket() ? preg_replace('/.pkt$/i','',$this->rawName()) : NULL;
|
||||
return $this->isPacket() ? $this->rawName() : NULL;
|
||||
}
|
||||
}
|
||||
}
|
@ -48,7 +48,7 @@ abstract class Base
|
||||
|
||||
// 4 BITS of type
|
||||
protected const IS_FILE = (1<<0);
|
||||
protected const IS_PKT = (1<<1);
|
||||
public const IS_PKT = (1<<1);
|
||||
protected const IS_ARC = (1<<2);
|
||||
protected const IS_REQ = (1<<3);
|
||||
protected const IS_TIC = (1<<4);
|
||||
@ -79,7 +79,7 @@ abstract class Base
|
||||
return ($this->ftype&0xff) & $type;
|
||||
}
|
||||
|
||||
protected function whatType(): int
|
||||
public function whatType(): int
|
||||
{
|
||||
static $ext = ['su','mo','tu','we','th','fr','sa','req'];
|
||||
|
||||
|
@ -86,7 +86,7 @@ final class File extends Send
|
||||
{
|
||||
// If sending file is a File::class, then our file is s3
|
||||
if ($this->nameas && $this->f instanceof FileModel) {
|
||||
$this->fd = Storage::readStream($this->f->full_storage_path);
|
||||
$this->fd = Storage::readStream($this->f->rel_name);
|
||||
|
||||
} else {
|
||||
$this->fd = fopen($this->full_name,'rb');
|
||||
|
@ -2,14 +2,13 @@
|
||||
|
||||
namespace App\Classes\File;
|
||||
|
||||
use Illuminate\Contracts\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
use App\Models\Address;
|
||||
|
||||
final class Item extends Receive
|
||||
{
|
||||
private const LOCATION = 'local';
|
||||
|
||||
/** @var Address The address that sent us this item */
|
||||
private Address $ao;
|
||||
private string $recvas;
|
||||
@ -34,14 +33,14 @@ final class Item extends Receive
|
||||
public function __get($key) {
|
||||
switch ($key) {
|
||||
case 'exists':
|
||||
return Storage::disk(self::LOCATION)->exists($this->rel_name);
|
||||
return Storage::disk(config('fido.local_disk'))->exists($this->rel_name);
|
||||
|
||||
case 'stor_name':
|
||||
return sprintf('%04X-%s',$this->ao->id,$this->recvas);
|
||||
case 'pref_name':
|
||||
return sprintf('%04X-%d-%s',$this->ao->id,$this->recvmtime,$this->recvas);
|
||||
case 'rel_name':
|
||||
return sprintf('%s/%s',config('app.fido'),$this->stor_name);
|
||||
return sprintf('%s/%s',config('fido.dir'),$this->pref_name);
|
||||
case 'full_name':
|
||||
return Storage::disk(self::LOCATION)->path($this->rel_name);
|
||||
return Storage::disk(config('fido.local_disk'))->path($this->rel_name);
|
||||
|
||||
case 'match_mtime':
|
||||
return $this->mtime === $this->recvmtime;
|
||||
@ -59,10 +58,10 @@ final class Item extends Receive
|
||||
return sprintf('%s %lu %lu',$this->recvas,$this->recvsize,$this->recvmtime);
|
||||
|
||||
case 'mtime':
|
||||
return Storage::disk(self::LOCATION)->lastModified($this->rel_name);
|
||||
return Storage::disk(config('fido.local_disk'))->lastModified($this->rel_name);
|
||||
|
||||
case 'size':
|
||||
return Storage::disk(self::LOCATION)->size($this->rel_name);
|
||||
return Storage::disk(config('fido.local_disk'))->size($this->rel_name);
|
||||
|
||||
default:
|
||||
return parent::__get($key);
|
||||
|
@ -5,14 +5,18 @@ namespace App\Classes\File;
|
||||
use Carbon\Carbon;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
use App\Classes\Node;
|
||||
use App\Classes\FTN\{Message,Packet};
|
||||
|
||||
final class Mail extends Send
|
||||
{
|
||||
private const LOGKEY = 'IFM';
|
||||
|
||||
/** @var int Our internal position counter */
|
||||
private int $readpos;
|
||||
private ?string $content;
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
@ -29,7 +33,7 @@ final class Mail extends Send
|
||||
public function __get($key) {
|
||||
switch ($key) {
|
||||
case 'dbids':
|
||||
return $this->f->messages->pluck('dbid');
|
||||
return $this->f->messages->pluck('id');
|
||||
|
||||
case 'name':
|
||||
return sprintf('%08x',timew($this->youngest()));
|
||||
@ -56,6 +60,8 @@ final class Mail extends Send
|
||||
if ($successful) {
|
||||
$this->complete = TRUE;
|
||||
|
||||
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)
|
||||
&& ($x=$this->dbids)->count())
|
||||
@ -79,6 +85,8 @@ final class Mail extends Send
|
||||
'sent_at'=>Carbon::now(),
|
||||
'sent_pkt'=>$this->name,
|
||||
]);
|
||||
|
||||
$this->content = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,12 +97,13 @@ final class Mail extends Send
|
||||
|
||||
public function open(string $compress=''): bool
|
||||
{
|
||||
$this->content = (string)$this->f;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
public function read(int $length): string
|
||||
{
|
||||
$result = substr((string)$this->f,$this->readpos,$length);
|
||||
$result = substr($this->content,$this->readpos,$length);
|
||||
$this->readpos += strlen($result);
|
||||
|
||||
return $result;
|
||||
@ -103,10 +112,10 @@ final class Mail extends Send
|
||||
public function seek(int $pos): bool
|
||||
{
|
||||
$this->readpos = ($pos < $this->size) ? $pos : $this->size;
|
||||
return TRUE;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
public function youngest(): Carbon
|
||||
private function youngest(): Carbon
|
||||
{
|
||||
return $this->f->messages->pluck('date')->sort()->last();
|
||||
}
|
||||
|
@ -5,15 +5,12 @@ namespace App\Classes\File;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
||||
|
||||
use App\Classes\{File,Protocol};
|
||||
use App\Classes\FTN\{InvalidPacketException,Packet};
|
||||
use App\Classes\Protocol;
|
||||
use App\Exceptions\FileGrewException;
|
||||
use App\Jobs\{MessageProcess,TicProcess};
|
||||
use App\Jobs\{PacketProcess,TicProcess};
|
||||
use App\Models\Address;
|
||||
use App\Notifications\Netmails\PacketPasswordInvalid;
|
||||
|
||||
/**
|
||||
* Object representing the files we are receiving
|
||||
@ -37,6 +34,7 @@ class Receive extends Base
|
||||
private ?string $comp;
|
||||
/** @var string|null The compressed data received */
|
||||
private ?string $comp_data;
|
||||
private $queue = FALSE;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@ -63,7 +61,7 @@ class Receive extends Base
|
||||
case 'nameas':
|
||||
case 'size':
|
||||
case 'name_size_time':
|
||||
case 'stor_name':
|
||||
case 'pref_name':
|
||||
return $this->receiving->{$key};
|
||||
|
||||
case 'pos':
|
||||
@ -117,98 +115,25 @@ class Receive extends Base
|
||||
|
||||
fclose($this->f);
|
||||
// Set our mtime
|
||||
Log::info(sprintf('%s:= Setting file [%s] to time [%s]',self::LOGKEY,$this->receiving->full_name,$this->receiving->recvmtime));
|
||||
Log::debug(sprintf('%s:= Setting file [%s] to time [%s]',self::LOGKEY,$this->receiving->full_name,$this->receiving->recvmtime));
|
||||
touch($this->receiving->full_name,$this->receiving->recvmtime);
|
||||
$this->f = NULL;
|
||||
|
||||
// If we received a packet, we'll dispatch a job to process it, if we got it all
|
||||
if ($this->receiving->complete)
|
||||
switch ($x=$this->receiving->whatType()) {
|
||||
switch ($this->receiving->whatType()) {
|
||||
case self::IS_ARC:
|
||||
case self::IS_PKT:
|
||||
Log::info(sprintf('%s:- Processing mail %s [%s]',self::LOGKEY,$x === self::IS_PKT ? 'PACKET' : 'ARCHIVE',$this->receiving->nameas));
|
||||
|
||||
try {
|
||||
$f = new File($this->receiving->full_name);
|
||||
$processed = FALSE;
|
||||
|
||||
foreach ($f as $packet) {
|
||||
$po = Packet::process($packet,Arr::get(stream_get_meta_data($packet),'uri'),$f->itemSize(),$this->ao->system);
|
||||
|
||||
// Check the messages are from the uplink
|
||||
if ($this->ao->system->addresses->search(function($item) use ($po) { return $item->id === $po->fftn_o->id; }) === FALSE) {
|
||||
Log::error(sprintf('%s:! Packet [%s] is not from this link? [%d]',self::LOGKEY,$po->fftn_o->ftn,$this->ao->system_id));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Check the packet password
|
||||
if ($this->ao->session('pktpass') !== $po->password) {
|
||||
Log::error(sprintf('%s:! Packet from [%s] with password [%s] is invalid.',self::LOGKEY,$this->ao->ftn,$po->password));
|
||||
|
||||
Notification::route('netmail',$this->ao)->notify(new PacketPasswordInvalid($po->password,$this->receiving->nameas));
|
||||
break;
|
||||
}
|
||||
|
||||
Log::info(sprintf('%s:- Packet has [%d] messages',self::LOGKEY,$po->count()));
|
||||
|
||||
// Queue messages if there are too many in the packet.
|
||||
if ($queue = ($po->count() > config('app.queue_msgs')))
|
||||
Log::info(sprintf('%s:- Messages will be sent to the queue for processing',self::LOGKEY));
|
||||
|
||||
$count = 0;
|
||||
foreach ($po as $msg) {
|
||||
Log::info(sprintf('%s:- Mail from [%s] to [%s]',self::LOGKEY,$msg->fftn,$msg->tftn));
|
||||
|
||||
// @todo Quick check that the packet should be processed by us.
|
||||
// @todo validate that the packet's zone is in the domain.
|
||||
|
||||
/*
|
||||
* // @todo generate exception when echomail for an area that doesnt exist
|
||||
* // @todo generate exception when echomail for an area sender cannot post to
|
||||
* // @todo generate exception when echomail for an area sender not subscribed to
|
||||
* // @todo generate exception when echomail comes from a system not defined here
|
||||
* // @todo generate exception when echomail comes from a system doesnt exist
|
||||
*
|
||||
* // @todo generate exception when netmail to system that doesnt exist (node/point)
|
||||
* // @todo generate exception when netmail from system that doesnt exist (node/point)
|
||||
* // @todo generate warning when netmail comes from a system not defined here
|
||||
*
|
||||
* // @todo generate exception when packet has wrong password
|
||||
*/
|
||||
|
||||
try {
|
||||
// Dispatch job.
|
||||
if ($queue)
|
||||
MessageProcess::dispatch($msg,$f->pktName(),$this->ao,$po->fftn_o,$rcvd_time);
|
||||
else
|
||||
MessageProcess::dispatchSync($msg,$f->pktName(),$this->ao,$po->fftn_o,$rcvd_time);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error(sprintf('%s:! Got error dispatching message [%s] (%d:%s-%s).',self::LOGKEY,$msg->msgid,$e->getLine(),$e->getFile(),$e->getMessage()));
|
||||
}
|
||||
|
||||
$count++;
|
||||
}
|
||||
|
||||
if ($count === $po->count())
|
||||
$processed = TRUE;
|
||||
}
|
||||
|
||||
if (! $processed) {
|
||||
Log::alert(sprintf('%s:- Not deleting packet [%s], it doesnt seem to be processed?',self::LOGKEY,$this->receiving->nameas));
|
||||
|
||||
// If we want to keep the packet, we could do that logic here
|
||||
} elseif (! config('app.packet_keep')) {
|
||||
Log::debug(sprintf('%s:- Deleting processed packet [%s]',self::LOGKEY,$this->receiving->full_name));
|
||||
unlink($this->receiving->full_name);
|
||||
}
|
||||
|
||||
} catch (InvalidPacketException $e) {
|
||||
Log::error(sprintf('%s:- Not deleting packet [%s], as it generated an InvalidPacketException',self::LOGKEY,$this->receiving->nameas),['e'=>$e->getMessage()]);
|
||||
// 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->zone->domain,FALSE,$rcvd_time);
|
||||
} else
|
||||
PacketProcess::dispatchSync($this->receiving->rel_name,$this->ao->zone->domain,TRUE,$rcvd_time);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error(sprintf('%s:- Not deleting packet [%s], as it generated an uncaught exception',self::LOGKEY,$this->receiving->nameas),['e'=>$e->getMessage()]);
|
||||
Log::error(sprintf('%s:! Got error dispatching packet [%s] (%d:%s-%s).',self::LOGKEY,$this->receiving->rel_name,$e->getLine(),$e->getFile(),$e->getMessage()));
|
||||
}
|
||||
|
||||
break;
|
||||
@ -217,7 +142,7 @@ class Receive extends Base
|
||||
Log::info(sprintf('%s:- Processing TIC file [%s]',self::LOGKEY,$this->receiving->nameas));
|
||||
|
||||
// Queue the tic to be processed later, in case the referenced file hasnt been received yet
|
||||
TicProcess::dispatch($this->receiving->rel_name);
|
||||
TicProcess::dispatch($this->receiving->pref_name)->delay(60);
|
||||
|
||||
break;
|
||||
|
||||
@ -232,11 +157,12 @@ class Receive extends Base
|
||||
/**
|
||||
* Add a new file to receive
|
||||
*
|
||||
* @param array $file
|
||||
* @param Address $ao
|
||||
* @param array $file The name of the receiving file
|
||||
* @param Address $ao Sender sending a file to us
|
||||
* @param bool $queue Force sending received items to the queue
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function new(array $file,Address $ao): void
|
||||
public function new(array $file,Address $ao,$queue=FALSE): void
|
||||
{
|
||||
Log::debug(sprintf('%s:+ Receiving new file [%s]',self::LOGKEY,join('|',$file)));
|
||||
|
||||
@ -244,6 +170,7 @@ class Receive extends Base
|
||||
throw new \Exception('Can only have 1 file receiving at a time');
|
||||
|
||||
$this->ao = $ao;
|
||||
$this->queue = $queue;
|
||||
|
||||
$this->list->push(new Item($ao,Arr::get($file,'name'),(int)Arr::get($file,'mtime'),(int)Arr::get($file,'size')));
|
||||
$this->index = $this->list->count()-1;
|
||||
|
@ -6,6 +6,7 @@ use Exception;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use League\Flysystem\UnreadableFileEncountered;
|
||||
|
||||
use App\Classes\File\Send\Dynamic;
|
||||
use App\Classes\Node;
|
||||
use App\Models\Address;
|
||||
|
||||
@ -122,13 +123,38 @@ class Send extends Base
|
||||
|
||||
if ($successful) {
|
||||
$end = time()-$this->start;
|
||||
Log::debug(sprintf('%s: - Closing [%s], sent in [%d] with [%s] items',self::LOGKEY,$this->sending->nameas,$end,$this->sending->dbids->count()));
|
||||
Log::info(sprintf('%s:- Closing [%s], sent in [%d] with [%s] items',self::LOGKEY,$this->sending->nameas,$end,$this->sending->dbids->count()));
|
||||
}
|
||||
|
||||
$this->sending->close($successful,$node);
|
||||
$this->index = NULL;
|
||||
}
|
||||
|
||||
public function dynamic(Address $ao): bool
|
||||
{
|
||||
$file = FALSE;
|
||||
|
||||
// If the node is marked as hold - dont send any files.
|
||||
if ($ao->system->hold) {
|
||||
Log::info(sprintf('%s:- System [%d] is marked as hold - not checking for files.',self::LOGKEY,$ao->system_id));
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Files
|
||||
if (($x=$ao->dynamicWaiting())->count()) {
|
||||
Log::debug(sprintf('%s:- [%d] Dynamic Files(s) added for sending to [%s]',self::LOGKEY,$x->count(),$ao->ftn));
|
||||
|
||||
// Add Files
|
||||
foreach ($x as $do)
|
||||
$this->list->push(new Dynamic($do,$ao,self::T_FILE));
|
||||
|
||||
$file = TRUE;
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/*
|
||||
private function compress(string $comp_mode): void
|
||||
{
|
||||
@ -167,7 +193,7 @@ class Send extends Base
|
||||
|
||||
// If the node is marked as hold - dont send any files.
|
||||
if ($ao->system->hold) {
|
||||
Log::info(sprintf('%s: - System [%d] is marked as hold - not checking for files.',self::LOGKEY,$ao->system_id));
|
||||
Log::info(sprintf('%s:- System [%d] is marked as hold - not checking for files.',self::LOGKEY,$ao->system_id));
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
@ -202,7 +228,7 @@ class Send extends Base
|
||||
if ((($this->index=$this->list->search(function($item) { return $item->complete === FALSE; })) !== FALSE)
|
||||
&& $this->sending->open())
|
||||
{
|
||||
Log::debug(sprintf('%s:- Sending item [%d] (%s)',self::LOGKEY,$this->index,$this->sending->nameas));
|
||||
Log::info(sprintf('%s:- Sending item [%d] (%s)',self::LOGKEY,$this->index,$this->sending->nameas));
|
||||
|
||||
$this->pos = 0;
|
||||
$this->start = time();
|
||||
@ -223,32 +249,31 @@ class Send extends Base
|
||||
* Add our mail to the send queue
|
||||
*
|
||||
* @param Address $ao
|
||||
* @param bool $update
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
*/
|
||||
public function mail(Address $ao,bool $update=TRUE): bool
|
||||
public function mail(Address $ao): bool
|
||||
{
|
||||
$mail = FALSE;
|
||||
|
||||
// If the node is marked as hold - dont send any mail.
|
||||
if ($ao->system->hold) {
|
||||
Log::info(sprintf('%s: - System [%d] is marked as hold - not checking for mail.',self::LOGKEY,$ao->system_id));
|
||||
Log::info(sprintf('%s:- System [%d] is marked as hold - not checking for mail.',self::LOGKEY,$ao->system_id));
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Netmail
|
||||
if ($x=$ao->getNetmail($update)) {
|
||||
Log::debug(sprintf('%s: - Netmail(s) added for sending to [%s]',self::LOGKEY,$ao->ftn));
|
||||
if ($x=$ao->getNetmail()) {
|
||||
Log::debug(sprintf('%s:- Netmail(s) added for sending to [%s]',self::LOGKEY,$ao->ftn));
|
||||
|
||||
$this->list->push(new Mail($x,self::T_NETMAIL));
|
||||
$mail = TRUE;
|
||||
}
|
||||
|
||||
// Echomail
|
||||
if ($x=$ao->getEchomail($update)) {
|
||||
Log::debug(sprintf('%s: - Echomail(s) added for sending to [%s]',self::LOGKEY,$ao->ftn));
|
||||
if ($x=$ao->getEchomail()) {
|
||||
Log::debug(sprintf('%s:- Echomail(s) added for sending to [%s]',self::LOGKEY,$ao->ftn));
|
||||
|
||||
$this->list->push(new Mail($x,self::T_ECHOMAIL));
|
||||
$mail = TRUE;
|
||||
|
124
app/Classes/File/Send/Dynamic.php
Normal file
124
app/Classes/File/Send/Dynamic.php
Normal file
@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\File\Send;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
use App\Classes\File\Send;
|
||||
use App\Classes\Node;
|
||||
use App\Models\Address;
|
||||
use App\Models\Dynamic as Model;
|
||||
use App\Classes\Dynamic as Item;
|
||||
|
||||
/**
|
||||
* Dynamic files that are sent to systems during a mailer session
|
||||
*/
|
||||
final class Dynamic extends Send
|
||||
{
|
||||
private const LOGKEY = 'FSD';
|
||||
|
||||
/** @var int Our internal position counter */
|
||||
private int $readpos = 0;
|
||||
private string $buffer;
|
||||
private Item $item;
|
||||
private Carbon $sent;
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct(private Model $do,Address $ao,int $type)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->ftype = ((($type&0xff)<<8)|self::IS_FILE);
|
||||
$this->item = new $this->do->model($ao,$this->do->arguments);
|
||||
$this->sent = Carbon::now();
|
||||
}
|
||||
|
||||
public function __get($key) {
|
||||
switch ($key) {
|
||||
case 'dbids':
|
||||
return collect([$this->do->id]);
|
||||
|
||||
case 'nameas':
|
||||
return $this->item->getName();
|
||||
|
||||
case 'mtime':
|
||||
return $this->sent->timestamp;
|
||||
|
||||
case 'size':
|
||||
return strlen($this->buffer);
|
||||
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
public function close(bool $successful,Node $node): void
|
||||
{
|
||||
if ($successful) {
|
||||
$this->complete = TRUE;
|
||||
|
||||
$next_at = $this->do->next_at
|
||||
->startOfDay()
|
||||
->addHours($this->do->start_time->hour)
|
||||
->addMinutes($this->do->start_time->minute);
|
||||
|
||||
switch ($this->do->frequency) {
|
||||
case 'ONCE':
|
||||
$this->do->active = FALSE;
|
||||
|
||||
break;
|
||||
|
||||
case 'DAILY':
|
||||
$this->do->next_at = $next_at
|
||||
->addDay();
|
||||
|
||||
break;
|
||||
|
||||
case 'WEEKLY':
|
||||
$this->do->next_at = $next_at
|
||||
->addWeek();
|
||||
|
||||
break;
|
||||
|
||||
case 'MONTHLY':
|
||||
$this->do->next_at = $next_at
|
||||
->addMonth();
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \Exception(sprintf('%s:! Unknown frequency [%s] for [%d]',self::LOGKEY,$this->do->frequency,$this->do->id));
|
||||
}
|
||||
|
||||
$this->do->save();
|
||||
}
|
||||
}
|
||||
|
||||
public function feof(): bool
|
||||
{
|
||||
return ($this->readpos === $this->size);
|
||||
}
|
||||
|
||||
public function open(string $compress=''): bool
|
||||
{
|
||||
$this->buffer = (string)$this->item;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
public function read(int $length): string
|
||||
{
|
||||
$result = substr($this->buffer,$this->readpos,$length);
|
||||
$this->readpos += strlen($result);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function seek(int $pos): bool
|
||||
{
|
||||
$this->readpos = ($pos < $this->size) ? $pos : $this->size;
|
||||
return TRUE;
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@ final class Tic extends Send
|
||||
$this->ftype = ((($type&0xff)<<8)|self::IS_TIC);
|
||||
$this->readpos = 0;
|
||||
|
||||
$this->tic = FTNTic::generate($ao,$file);
|
||||
$this->tic = (string)(new FTNTic($file))->to($ao);
|
||||
}
|
||||
|
||||
public function __get($key) {
|
||||
|
@ -92,7 +92,7 @@ class Node
|
||||
|
||||
// Return how long our session has been connected
|
||||
case 'session_time':
|
||||
return Carbon::now()->diffInSeconds($this->start_time);
|
||||
return sprintf("%d",$this->start_time->diffInSeconds(Carbon::now()));
|
||||
|
||||
case 'system':
|
||||
case 'sysop':
|
||||
|
@ -186,6 +186,8 @@ class Page
|
||||
$this->text .= $text;
|
||||
|
||||
$this->text_right = $right;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -9,7 +9,9 @@ use App\Classes\File\{Receive,Send};
|
||||
use App\Classes\Protocol\EMSI;
|
||||
use App\Classes\Sock\SocketClient;
|
||||
use App\Classes\Sock\SocketException;
|
||||
use App\Models\{Address,Setup,System,SystemLog};
|
||||
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.
|
||||
|
||||
abstract class Protocol
|
||||
{
|
||||
@ -26,14 +28,6 @@ abstract class Protocol
|
||||
protected const RCDO = -3;
|
||||
protected const ERROR = -5;
|
||||
|
||||
// Our sessions Types
|
||||
public const SESSION_AUTO = 0;
|
||||
/** @deprecate Use mailers:class */
|
||||
public const SESSION_EMSI = 1;
|
||||
/** @deprecate Use mailers:class */
|
||||
public const SESSION_BINKP = 2;
|
||||
public const SESSION_ZMODEM = 3;
|
||||
|
||||
protected const MAX_PATH = 1024;
|
||||
|
||||
/* O_ options - [First 9 bits are protocol P_* (Interfaces/ZModem)] */
|
||||
@ -132,9 +126,11 @@ abstract class Protocol
|
||||
|
||||
private array $comms;
|
||||
|
||||
protected bool $force_queue = FALSE;
|
||||
|
||||
abstract protected function protocol_init(): int;
|
||||
|
||||
abstract protected function protocol_session(): int;
|
||||
abstract protected function protocol_session(bool $force_queue=FALSE): int;
|
||||
|
||||
public function __construct(Setup $o=NULL)
|
||||
{
|
||||
@ -174,6 +170,10 @@ abstract class Protocol
|
||||
$this->comms[$key] = $value;
|
||||
break;
|
||||
|
||||
case 'client':
|
||||
$this->{$key} = $value;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \Exception('Unknown key: '.$key);
|
||||
}
|
||||
@ -309,14 +309,14 @@ abstract class Protocol
|
||||
$addresses = collect();
|
||||
|
||||
foreach (($this->originate ? $this->node->aka_remote_authed : $this->node->aka_remote) as $ao)
|
||||
$addresses = $addresses->merge($this->setup->system->match($ao->zone,Address::NODE_ZC|Address::NODE_RC|Address::NODE_NC|Address::NODE_HC|Address::NODE_ACTIVE|Address::NODE_PVT|Address::NODE_POINT));
|
||||
$addresses = $addresses->merge(our_address($ao->zone->domain));
|
||||
|
||||
$addresses = $addresses->unique();
|
||||
|
||||
Log::debug(sprintf('%s:- Presenting limited AKAs [%s]',self::LOGKEY,$addresses->pluck('ftn')->join(',')));
|
||||
|
||||
} else {
|
||||
$addresses = $this->setup->system->addresses;
|
||||
$addresses = $this->setup->system->akas;
|
||||
|
||||
Log::debug(sprintf('%s:- Presenting ALL our AKAs [%s]',self::LOGKEY,$addresses->pluck('ftn')->join(',')));
|
||||
}
|
||||
@ -327,13 +327,13 @@ abstract class Protocol
|
||||
/**
|
||||
* Initialise our Session
|
||||
*
|
||||
* @param int $type
|
||||
* @param Mailer $mo
|
||||
* @param SocketClient $client
|
||||
* @param Address|null $o
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function session(int $type,SocketClient $client,Address $o=NULL): int
|
||||
public function session(Mailer $mo,SocketClient $client,Address $o=NULL): int
|
||||
{
|
||||
if ($o->exists)
|
||||
Log::withContext(['ftn'=>$o->ftn]);
|
||||
@ -365,12 +365,11 @@ abstract class Protocol
|
||||
|
||||
// We are an IP node
|
||||
$this->optionSet(self::O_TCP);
|
||||
$this->setClient($client);
|
||||
$this->client = $client;
|
||||
|
||||
switch ($type) {
|
||||
/** @noinspection PhpMissingBreakStatementInspection */
|
||||
case self::SESSION_AUTO:
|
||||
Log::debug(sprintf('%s:- Trying EMSI',self::LOGKEY));
|
||||
switch ($mo->name) {
|
||||
case 'EMSI':
|
||||
Log::debug(sprintf('%s:- Starting EMSI',self::LOGKEY));
|
||||
|
||||
$rc = $this->protocol_init();
|
||||
if ($rc < 0) {
|
||||
@ -379,27 +378,19 @@ abstract class Protocol
|
||||
return self::S_FAILURE;
|
||||
}
|
||||
|
||||
case self::SESSION_EMSI:
|
||||
Log::debug(sprintf('%s:- Starting EMSI',self::LOGKEY));
|
||||
$rc = $this->protocol_session();
|
||||
$rc = $this->protocol_session($this->originate);
|
||||
|
||||
break;
|
||||
|
||||
case self::SESSION_BINKP:
|
||||
case 'BINKP':
|
||||
Log::debug(sprintf('%s:- Starting BINKP',self::LOGKEY));
|
||||
$rc = $this->protocol_session();
|
||||
|
||||
$rc = $this->protocol_session($this->originate);
|
||||
|
||||
break;
|
||||
|
||||
case self::SESSION_ZMODEM:
|
||||
Log::debug(sprintf('%s:- Starting ZMODEM',self::LOGKEY));
|
||||
$this->client->speed = self::TCP_SPEED;
|
||||
$this->originate = FALSE;
|
||||
|
||||
return $this->protocol_session();
|
||||
|
||||
default:
|
||||
Log::error(sprintf('%s:! Unsupported session type [%d]',self::LOGKEY,$type));
|
||||
Log::error(sprintf('%s:! Unsupported session type [%d]',self::LOGKEY,$mo->id));
|
||||
|
||||
return self::S_FAILURE;
|
||||
}
|
||||
@ -429,16 +420,17 @@ abstract class Protocol
|
||||
));
|
||||
|
||||
// Add unknown FTNs to the DB
|
||||
if ($this->node->aka_remote_authed->count()) {
|
||||
$so = $this->node->aka_remote_authed->first()->system;
|
||||
} else {
|
||||
$so = System::where('name','Discovered System')->single();
|
||||
}
|
||||
$so = ($this->node->aka_remote_authed->count())
|
||||
? $this->node->aka_remote_authed->first()->system
|
||||
: System::createUnknownSystem();
|
||||
|
||||
if ($so && $so->exists) {
|
||||
foreach ($this->node->aka_other as $aka) {
|
||||
Address::findFTN($aka,TRUE,$so);
|
||||
}
|
||||
foreach ($this->node->aka_other as $aka)
|
||||
if (! Address::findFTN($aka)) {
|
||||
$oo = Address::createFTN($aka,$so);
|
||||
$oo->validated = TRUE;
|
||||
$oo->save();
|
||||
}
|
||||
|
||||
// Log session in DB
|
||||
$slo = new SystemLog;
|
||||
@ -446,12 +438,18 @@ 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->sessiontype = $type;
|
||||
$slo->mailer_id = $mo->id;
|
||||
$slo->sessiontime = $this->node->session_time;
|
||||
$slo->result = ($rc & self::S_MASK);
|
||||
$slo->originate = $this->originate;
|
||||
|
||||
$so->logs()->save($slo);
|
||||
|
||||
// If we are autohold, then remove that
|
||||
if ($so->autohold) {
|
||||
$so->autohold = FALSE;
|
||||
$so->save();
|
||||
}
|
||||
}
|
||||
|
||||
// @todo Optional after session execution event
|
||||
@ -495,15 +493,4 @@ abstract class Protocol
|
||||
{
|
||||
$this->session |= $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set our client that we are communicating with
|
||||
*
|
||||
* @param SocketClient $client
|
||||
* @deprecated use __get()/__set()
|
||||
*/
|
||||
protected function setClient(SocketClient $client): void
|
||||
{
|
||||
$this->client = $client;
|
||||
}
|
||||
}
|
@ -9,11 +9,12 @@ use Illuminate\Support\Facades\Log;
|
||||
use League\Flysystem\UnreadableFileEncountered;
|
||||
|
||||
use App\Classes\Crypt;
|
||||
use App\Classes\Node;
|
||||
use App\Classes\Protocol as BaseProtocol;
|
||||
use App\Classes\Sock\SocketClient;
|
||||
use App\Classes\Sock\SocketException;
|
||||
use App\Exceptions\FileGrewException;
|
||||
use App\Models\Address;
|
||||
use App\Exceptions\{FileGrewException,InvalidFTNException};
|
||||
use App\Models\{Address,Mailer};
|
||||
|
||||
final class Binkp extends BaseProtocol
|
||||
{
|
||||
@ -147,7 +148,6 @@ final class Binkp extends BaseProtocol
|
||||
* @param SocketClient $client
|
||||
* @return int|null
|
||||
* @throws SocketException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function onConnect(SocketClient $client): ?int
|
||||
{
|
||||
@ -155,8 +155,7 @@ final class Binkp extends BaseProtocol
|
||||
if (! parent::onConnect($client)) {
|
||||
Log::withContext(['pid'=>getmypid()]);
|
||||
|
||||
// @todo If we can use SESSION_EMSI set an object class value that in BINKP of SESSION_BINKP, and move this method to the parent class
|
||||
$this->session(self::SESSION_BINKP,$client,(new Address));
|
||||
$this->session(Mailer::where('name','BINKP')->singleOrFail(),$client,(new Address));
|
||||
$this->client->close();
|
||||
exit(0);
|
||||
}
|
||||
@ -431,17 +430,23 @@ final class Binkp extends BaseProtocol
|
||||
$rc = TRUE;
|
||||
|
||||
} else {
|
||||
$data = substr($this->rx_buf,1);
|
||||
// http://ftsc.org/docs/fts-1026.001 - frames may be NULL terminated
|
||||
$data = rtrim(substr($this->rx_buf,1),"\x00");
|
||||
|
||||
switch ($msg) {
|
||||
case self::BPM_ADR:
|
||||
Log::debug(sprintf('%s:- ADR:Address [%s]',self::LOGKEY,$data));
|
||||
$rc = $this->M_adr($data);
|
||||
// @note It seems taurus may pad data with nulls at the end (esp BPM_ADR), so we should trim that.
|
||||
$rc = $this->M_adr(trim($data));
|
||||
break;
|
||||
|
||||
case self::BPM_EOB:
|
||||
Log::debug(sprintf('%s:- EOB:We got an EOB message with [%d] chars in the buffer',self::LOGKEY,strlen($data)));
|
||||
$rc = $this->M_eob($data);
|
||||
|
||||
if (strlen($data))
|
||||
Log::critical(sprintf('%s:! EOB but we have data?',self::LOGKEY),['data'=>$data]);
|
||||
|
||||
$rc = $this->M_eob();
|
||||
break;
|
||||
|
||||
case self::BPM_NUL:
|
||||
@ -451,7 +456,7 @@ final class Binkp extends BaseProtocol
|
||||
|
||||
case self::BPM_PWD:
|
||||
Log::debug(sprintf('%s:- PWD:We got a password [%s]',self::LOGKEY,$data));
|
||||
$rc = $this->M_pwd($data);
|
||||
$rc = $this->M_pwd(ltrim($data));
|
||||
break;
|
||||
|
||||
case self::BPM_ERR:
|
||||
@ -476,7 +481,7 @@ final class Binkp extends BaseProtocol
|
||||
|
||||
case self::BPM_OK:
|
||||
Log::debug(sprintf('%s:- OK:Got an OK [%s]',self::LOGKEY,$data));
|
||||
$rc = $this->M_ok($data);
|
||||
$rc = $this->M_ok(ltrim($data));
|
||||
break;
|
||||
|
||||
case self::BPM_CHAT:
|
||||
@ -680,26 +685,44 @@ final class Binkp extends BaseProtocol
|
||||
*/
|
||||
private function M_adr(string $buf): bool
|
||||
{
|
||||
$buf = $this->skip_blanks($buf);
|
||||
$rc = 0;
|
||||
|
||||
while ($rem_aka=$this->strsep($buf,' ')) {
|
||||
try {
|
||||
if (! ($o=Address::findFTN($rem_aka,FALSE,NULL,TRUE))) {
|
||||
if (! ($o=Address::findFTN($rem_aka,TRUE))) {
|
||||
// @todo when we have multiple inactive records, this returns more than 1, so pluck the active record if there is one
|
||||
Log::alert(sprintf('%s:? AKA is UNKNOWN [%s]',self::LOGKEY,$rem_aka));
|
||||
|
||||
$this->node->ftn_other = $rem_aka;
|
||||
continue;
|
||||
|
||||
// If we only present limited AKAs dont validate password against akas outside of the domains we present
|
||||
} elseif (is_null(our_address($o))) {
|
||||
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;
|
||||
|
||||
} elseif (! $o->active) {
|
||||
Log::alert(sprintf('%s:/ AKA is not active [%s], ignoring',self::LOGKEY,$rem_aka));
|
||||
Log::alert(sprintf('%s:/ AKA is not active [%s] - ignoring',self::LOGKEY,$rem_aka));
|
||||
|
||||
$this->node->ftn_other = $rem_aka;
|
||||
continue;
|
||||
|
||||
} else {
|
||||
Log::info(sprintf('%s:- Got AKA [%s]',self::LOGKEY,$rem_aka));
|
||||
|
||||
// We'll update this address status
|
||||
$o->validated = TRUE;
|
||||
$o->role &= ~(Address::NODE_HOLD|Address::NODE_DOWN);
|
||||
$o->save();
|
||||
}
|
||||
|
||||
} catch (InvalidFTNException $e) {
|
||||
Log::error(sprintf('%s:! AKA is INVALID [%s] (%s) - ignoring',self::LOGKEY,$rem_aka,$e->getMessage()));
|
||||
|
||||
continue;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error(sprintf('%s:! AKA is INVALID [%s] (%d:%s-%s)',self::LOGKEY,$rem_aka,$e->getLine(),$e->getFile(),$e->getMessage()));
|
||||
|
||||
@ -744,18 +767,14 @@ final class Binkp extends BaseProtocol
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Add our mail to the queue if we have authenticated
|
||||
if ($this->node->aka_authed)
|
||||
foreach ($this->node->aka_remote_authed as $ao) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* http://ftsc.org/docs/fts-1026.001
|
||||
* M_NUL "TRF netmail_bytes arcmail_bytes"
|
||||
* traffic prognosis (in bytes) for the netmail
|
||||
* (netmail_bytes) and arcmail + files (arcmail_bytes),
|
||||
* both are decimal ASCII strings
|
||||
*/
|
||||
// @todo This is affectively redundant, because we are not determining our mail until later
|
||||
$this->msgs(self::BPM_NUL,sprintf('TRF %lu %lu',$this->send->mail_size,$this->send->files_size));
|
||||
|
||||
if ($this->md_challenge) {
|
||||
@ -805,7 +824,7 @@ final class Binkp extends BaseProtocol
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function M_eob(string $buf): bool
|
||||
private function M_eob(): bool
|
||||
{
|
||||
if ($this->recv->fd) {
|
||||
Log::info(sprintf('%s:= Closing receiving file.',self::LOGKEY));
|
||||
@ -817,21 +836,8 @@ final class Binkp extends BaseProtocol
|
||||
$this->sessionClear(self::SE_DELAYEOB);
|
||||
|
||||
if (! $this->send->togo_count && $this->sessionGet(self::SE_NOFILES) && $this->capGet(self::F_MULTIBATCH,self::O_YES)) {
|
||||
// Add our mail to the queue if we have authenticated
|
||||
if ($this->node->aka_authed)
|
||||
foreach ($this->node->aka_remote_authed as $ao) {
|
||||
Log::debug(sprintf('%s:- Checking for any new mail and files to [%s]',self::LOGKEY,$ao->ftn));
|
||||
$this->getFiles($this->node);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Log::debug(sprintf('%s:- We have [%d] items to send to [%s]',self::LOGKEY,$this->send->togo_count,$ao->ftn));
|
||||
if ($this->send->togo_count)
|
||||
$this->sessionClear(self::SE_NOFILES|self::SE_SENTEOB);
|
||||
}
|
||||
@ -892,7 +898,7 @@ final class Binkp extends BaseProtocol
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
$this->recv->new($file['file'],$this->node->address);
|
||||
$this->recv->new($file['file'],$this->node->address,$this->force_queue);
|
||||
|
||||
try {
|
||||
switch ($this->recv->open($file['offs']<0,$file['flags'])) {
|
||||
@ -1012,6 +1018,9 @@ final class Binkp extends BaseProtocol
|
||||
|
||||
$this->send->close(TRUE,$this->node);
|
||||
}
|
||||
|
||||
} else {
|
||||
Log::error(sprintf('%s:! M_got[skip] not for our file? [%s]',self::LOGKEY,$buf));
|
||||
}
|
||||
|
||||
} else {
|
||||
@ -1029,16 +1038,16 @@ final class Binkp extends BaseProtocol
|
||||
Log::info(sprintf('%s:+ M_NUL [%s]',self::LOGKEY,$buf));
|
||||
|
||||
if (! strncmp($buf,'SYS ',4)) {
|
||||
$this->node->system = $this->skip_blanks(substr($buf,4));
|
||||
$this->node->system = ltrim(substr($buf,4));
|
||||
|
||||
} elseif (! strncmp($buf, 'ZYZ ',4)) {
|
||||
$this->node->sysop = $this->skip_blanks(substr($buf,4));
|
||||
$this->node->sysop = ltrim(substr($buf,4));
|
||||
|
||||
} elseif (! strncmp($buf,'LOC ',4)) {
|
||||
$this->node->location = $this->skip_blanks(substr($buf,4));
|
||||
$this->node->location = ltrim(substr($buf,4));
|
||||
|
||||
} elseif (! strncmp($buf,'NDL ',4)) {
|
||||
$data = $this->skip_blanks(substr($buf,4));
|
||||
$data = ltrim(substr($buf,4));
|
||||
$comma = strpos($data,',');
|
||||
$spd = substr($data,0,$comma);
|
||||
|
||||
@ -1049,7 +1058,7 @@ final class Binkp extends BaseProtocol
|
||||
$this->client->speed = $spd;
|
||||
|
||||
} else {
|
||||
$comma = $this->skip_blanks(substr($buf,4));
|
||||
$comma = ltrim(substr($buf,4));
|
||||
$c = 0;
|
||||
while (($x=substr($comma,$c,1)) && is_numeric($x))
|
||||
$c++;
|
||||
@ -1071,19 +1080,25 @@ final class Binkp extends BaseProtocol
|
||||
}
|
||||
|
||||
} elseif (! strncmp($buf,'TIME ',5)) {
|
||||
$this->node->node_time = $this->skip_blanks(substr($buf,5));
|
||||
$this->node->node_time = ltrim(substr($buf,5));
|
||||
|
||||
} elseif (! strncmp($buf,'VER ',4)) {
|
||||
$data = $this->skip_blanks(substr($buf,4));
|
||||
$data = ltrim(substr($buf,4));
|
||||
$matches = [];
|
||||
preg_match('#^(.+)\s+binkp/([0-9]+)\.([0-9]+)$#',$data,$matches);
|
||||
preg_match('#^(.+)\s+\(?binkp/([0-9]+)\.([0-9]+)\)?$#',$data,$matches);
|
||||
|
||||
$this->node->software = $matches[1];
|
||||
$this->node->ver_major = $matches[2];
|
||||
$this->node->ver_minor = $matches[3];
|
||||
if (count($matches) === 4) {
|
||||
$this->node->software = $matches[1];
|
||||
$this->node->ver_major = $matches[2];
|
||||
$this->node->ver_minor = $matches[3];
|
||||
} else {
|
||||
$this->node->software = 'Unknown';
|
||||
$this->node->ver_major = 0;
|
||||
$this->node->ver_minor = 0;
|
||||
}
|
||||
|
||||
} elseif (! strncmp($buf,'TRF ',4)) {
|
||||
$data = $this->skip_blanks(substr($buf,4));
|
||||
$data = ltrim(substr($buf,4));
|
||||
$matches = [];
|
||||
preg_match('/^([0-9]+)\s+([0-9]+)$/',$data,$matches);
|
||||
|
||||
@ -1097,13 +1112,13 @@ final class Binkp extends BaseProtocol
|
||||
$this->sessionSet(self::SE_DELAYEOB);
|
||||
|
||||
} elseif (! strncmp($buf,'PHN ',4)) {
|
||||
$this->node->phone = $this->skip_blanks(substr($buf,4));
|
||||
$this->node->phone = ltrim(substr($buf,4));
|
||||
|
||||
} elseif (! strncmp($buf,'OPM ',4)) {
|
||||
$this->node->message = $this->skip_blanks(substr($buf,4));
|
||||
$this->node->message = ltrim(substr($buf,4));
|
||||
|
||||
} elseif (! strncmp($buf,'OPT ',4)) {
|
||||
$data = $this->skip_blanks(substr($buf,4));
|
||||
$data = ltrim(substr($buf,4));
|
||||
|
||||
while ($data && ($p = $this->strsep($data,' '))) {
|
||||
if (! strcmp($p,'MB')) {
|
||||
@ -1142,12 +1157,15 @@ final class Binkp extends BaseProtocol
|
||||
$this->capSet(self::F_COMP,self::O_THEY|self::O_EXT);
|
||||
|
||||
} elseif (! strncmp($p,'CRAM-MD5-',9) && $this->originate && $this->capGet(self::F_MD,self::O_WANT)) {
|
||||
if (($x=strlen(substr($p,9))) > 64 ) {
|
||||
Log::error(sprintf('%s:! The challenge string is TOO LONG [%d] (%s)',self::LOGKEY,$x,$p));
|
||||
if (strlen($hex=substr($p,9)) > 64 ) {
|
||||
Log::error(sprintf('%s:! The challenge string is TOO LONG [%d] (%s)',self::LOGKEY,strlen($hex),$p));
|
||||
|
||||
} elseif (strlen($hex)%2) {
|
||||
Log::error(sprintf('%s:! The challenge string is an odd size [%d] (%s)',self::LOGKEY,strlen($hex),$hex));
|
||||
|
||||
} else {
|
||||
Log::info(sprintf('%s:- Remote wants MD5 auth',self::LOGKEY));
|
||||
$this->md_challenge = hex2bin(substr($p,9));
|
||||
Log::info(sprintf('%s:- Remote wants MD5 auth with [%s]',self::LOGKEY,$hex));
|
||||
$this->md_challenge = hex2bin($hex);
|
||||
|
||||
if ($this->md_challenge)
|
||||
$this->capSet(self::F_MD,self::O_THEY);
|
||||
@ -1184,8 +1202,6 @@ final class Binkp extends BaseProtocol
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$buf = $this->skip_blanks($buf);
|
||||
|
||||
if ($this->optionGet(self::O_PWD) && $buf) {
|
||||
while (($t=$this->strsep($buf," \t")))
|
||||
if (strcmp($t,'non-secure') === 0) {
|
||||
@ -1208,11 +1224,10 @@ final class Binkp extends BaseProtocol
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
* @todo It appears when we poll a node, we dont ask for passwords, but we still send echomail and files.
|
||||
*/
|
||||
private function M_pwd(string $buf): bool
|
||||
{
|
||||
$buf = $this->skip_blanks($buf);
|
||||
$have_CRAM = !strncasecmp($buf,'CRAM-MD5-',9);
|
||||
$have_pwd = $this->optionGet(self::O_PWD);
|
||||
|
||||
@ -1294,24 +1309,13 @@ final class Binkp extends BaseProtocol
|
||||
if (strlen($opt))
|
||||
$this->msgs(self::BPM_NUL,sprintf('OPT%s',$opt));
|
||||
|
||||
// Add our mail to the queue if we have authenticated
|
||||
// @todo This is effectively redundant, because we are not getting files until later
|
||||
$this->msgs(self::BPM_NUL,sprintf('TRF %lu %lu',$this->send->mail_size,$this->send->files_size));
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
$this->send->mail($ao);
|
||||
$this->send->files($ao);
|
||||
}
|
||||
|
||||
$this->msgs(self::BPM_NUL,sprintf('TRF %lu %lu',$this->send->mail_size,$this->send->files_size));
|
||||
$this->msgs(self::BPM_OK,sprintf('%ssecure',$have_pwd ? '' : 'non-'));
|
||||
|
||||
} else {
|
||||
// @todo Send any direct netmail to this node, if that node is unknown to us
|
||||
$this->msgs(self::BPM_NUL,sprintf('TRF %lu %lu',$this->send->mail_size,$this->send->files_size));
|
||||
$this->msgs(self::OK,'non-secure');
|
||||
}
|
||||
|
||||
@ -1327,14 +1331,16 @@ final class Binkp extends BaseProtocol
|
||||
/**
|
||||
* Set up our BINKP session
|
||||
*
|
||||
* @param bool $force_queue
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function protocol_session(): int
|
||||
protected function protocol_session(bool $force_queue=FALSE): int
|
||||
{
|
||||
if ($this->binkp_init() !== self::OK)
|
||||
return self::S_FAILURE;
|
||||
|
||||
$this->force_queue = $force_queue;
|
||||
$this->binkp_hs();
|
||||
|
||||
while (TRUE) {
|
||||
@ -1344,9 +1350,12 @@ final class Binkp extends BaseProtocol
|
||||
&& (! $this->sessionGet(self::SE_NOFILES))
|
||||
&& (! $this->send->fd))
|
||||
{
|
||||
if (! $this->send->togo_count)
|
||||
$this->getFiles($this->node);
|
||||
|
||||
// Open our next file to send
|
||||
if ($this->send->togo_count && ! $this->send->fd) {
|
||||
Log::info(sprintf('%s:- Opening next file to send',self::LOGKEY));
|
||||
Log::info(sprintf('%s:- Opening next file to send - we have [%d] left',self::LOGKEY,$this->send->togo_count));
|
||||
$this->send->open();
|
||||
}
|
||||
|
||||
@ -1374,6 +1383,8 @@ final class Binkp extends BaseProtocol
|
||||
// We dont have anything to send
|
||||
} else {
|
||||
Log::info(sprintf('%s:- Nothing left to send in this batch',self::LOGKEY));
|
||||
// @todo We should look for more mail/files before thinking about sending an EOB
|
||||
// IE: When we are set to only send X messages, but we have > X to send, get the next batch.
|
||||
$this->sessionSet(self::SE_NOFILES);
|
||||
}
|
||||
}
|
||||
@ -1478,13 +1489,50 @@ final class Binkp extends BaseProtocol
|
||||
return $this->rc;
|
||||
}
|
||||
|
||||
public function getFiles(Node $node): void
|
||||
{
|
||||
// Add our mail to the queue if we have authenticated
|
||||
if ($node->aka_authed) {
|
||||
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::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);
|
||||
$this->send->dynamic($ao);
|
||||
|
||||
/*
|
||||
* Add "dynamic files", eg: nodelist, nodelist segment, status reports.
|
||||
* Dynamic files are built on the fly
|
||||
* * query "dynamic" for items for the address
|
||||
* * column 'method' identifies the method that will be called, with the $ao as the argument
|
||||
* * a 'new Item' is added to the queue
|
||||
* * when it its ready to be sent, the __tostring() is called that renders it
|
||||
* * when sent, the dynamic table is updated with the sent_at
|
||||
*/
|
||||
}
|
||||
|
||||
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::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
|
||||
* @todo Can this be replaced with ltrim?
|
||||
* @deprecated - use ltrim instead
|
||||
*/
|
||||
private function skip_blanks(string $str): string
|
||||
{
|
||||
@ -1524,6 +1572,7 @@ final class Binkp extends BaseProtocol
|
||||
* @param string $str
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
* @deprecated No longer required since we are using ltrim
|
||||
*/
|
||||
private function isSpace(string $str):bool
|
||||
{
|
||||
|
@ -7,11 +7,11 @@ use Illuminate\Support\Str;
|
||||
|
||||
use App\Classes\Protocol as BaseProtocol;
|
||||
use App\Classes\Sock\SocketClient;
|
||||
use App\Http\Controllers\DomainController;
|
||||
use App\Models\{Address,Domain};
|
||||
use App\Models\{Address,Domain,Mailer};
|
||||
|
||||
/**
|
||||
* Respond to DNS queries and provide addresses to FTN nodes.
|
||||
* http://ftsc.org/docs/fts-5004.001
|
||||
*
|
||||
* This implementation doesnt support EDNS nor DNSSEC.
|
||||
*
|
||||
@ -59,8 +59,8 @@ final class DNS extends BaseProtocol
|
||||
public const DNS_TYPE_SOA = 6; // SOA Records
|
||||
public const DNS_TYPE_MX = 15; // MX Records
|
||||
public const DNS_TYPE_TXT = 16; // TXT Records
|
||||
|
||||
public const DNS_TYPE_AAAA = 28; // AAAA Records
|
||||
public const DNS_TYPE_SRV = 33; // SRV Records
|
||||
public const DNS_TYPE_OPT = 41; // OPT Records
|
||||
public const DNS_TYPE_DS = 43; // DS Records (Delegation signer RFC 4034)
|
||||
|
||||
@ -70,7 +70,7 @@ final class DNS extends BaseProtocol
|
||||
if (! parent::onConnect($client)) {
|
||||
Log::withContext(['pid'=>getmypid()]);
|
||||
|
||||
$this->setClient($client);
|
||||
$this->client = $client;
|
||||
$this->protocol_session();
|
||||
|
||||
Log::info(sprintf('%s:= onConnect - Connection closed [%s]',self::LOGKEY,$client->address_remote));
|
||||
@ -90,6 +90,7 @@ final class DNS extends BaseProtocol
|
||||
* Handle a DNS query
|
||||
*
|
||||
* https://www.ietf.org/rfc/rfc1035.txt
|
||||
* https://www.ietf.org/rfc/rfc2308.txt
|
||||
* https://github.com/guyinatuxedo/dns-fuzzer/blob/master/dns.md
|
||||
*
|
||||
* labels 63 octets or less
|
||||
@ -97,21 +98,29 @@ final class DNS extends BaseProtocol
|
||||
* TTL positive values of a signed 32 bit number.
|
||||
* UDP messages 512 octets or less
|
||||
*
|
||||
* @param bool $force_queue Not used here
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function protocol_session(): int
|
||||
public function protocol_session(bool $force_queue=FALSE): int
|
||||
{
|
||||
Log::debug(sprintf('%s:+ DNS Query',self::LOGKEY));
|
||||
|
||||
$this->query = new BaseProtocol\DNS\Query($this->client->read(0,512));
|
||||
try {
|
||||
$this->query = new BaseProtocol\DNS\Query($this->client->read(0,512));
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error(sprintf('%s:! Ignoring bad DNS query (%s)',self::LOGKEY,$e->getMessage()));
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
Log::info(sprintf('%s:= DNS Query from [%s] for [%s]',self::LOGKEY,$this->client->address_remote,$this->query->domain));
|
||||
|
||||
// If the wrong class
|
||||
if ($this->query->class !== self::DNS_QUERY_IN) {
|
||||
Log::error(sprintf('%s:! We only service Internet queries [%d]',self::LOGKEY,$this->query->class));
|
||||
return $this->reply(self::DNS_NOTIMPLEMENTED);
|
||||
return $this->reply(self::DNS_NOTIMPLEMENTED,[],$this->soa());
|
||||
}
|
||||
|
||||
$dos = Domain::select(['id','name','dnsdomain'])->active();
|
||||
@ -140,17 +149,9 @@ final class DNS extends BaseProtocol
|
||||
|
||||
return $this->reply(
|
||||
self::DNS_NOERROR,
|
||||
[serialize([
|
||||
$this->domain_split(gethostname()),
|
||||
$this->domain_split(Str::replace('@','.',config('app.mail.mail_from','nobody@'.gethostname()))),
|
||||
1,
|
||||
self::DEFAULT_TTL,
|
||||
self::DEFAULT_TTL,
|
||||
self::DEFAULT_TTL,
|
||||
self::DEFAULT_TTL
|
||||
]) => self::DNS_TYPE_SOA],
|
||||
$this->soa(),
|
||||
[],
|
||||
[serialize($this->domain_split(gethostname())) => self::DNS_TYPE_NS],
|
||||
[serialize($this->domain_split(config('fido.dns_ns'))) => self::DNS_TYPE_NS],
|
||||
);
|
||||
|
||||
case self::DNS_TYPE_NS:
|
||||
@ -158,23 +159,52 @@ final class DNS extends BaseProtocol
|
||||
|
||||
return $this->reply(
|
||||
self::DNS_NOERROR,
|
||||
[serialize($this->domain_split(gethostname())) => self::DNS_TYPE_NS]);
|
||||
[serialize($this->domain_split(config('fido.dns_ns'))) => self::DNS_TYPE_NS]);
|
||||
|
||||
// Respond to A/AAAA/CNAME queries, with value or NAMEERR
|
||||
case self::DNS_TYPE_CNAME:
|
||||
case self::DNS_TYPE_A:
|
||||
case self::DNS_TYPE_AAAA:
|
||||
case self::DNS_TYPE_SRV:
|
||||
case self::DNS_TYPE_TXT:
|
||||
Log::info(sprintf('%s:= Looking for record [%s] for [%s]',self::LOGKEY,$this->query->type,$this->query->domain));
|
||||
|
||||
$labels = clone($this->query->labels);
|
||||
$mailer = '';
|
||||
|
||||
// If this is a SRV record query
|
||||
if ($this->query->type === self::DNS_TYPE_SRV) {
|
||||
if ($labels->skip(1)->first() !== '_tcp')
|
||||
return $this->reply(self::DNS_NAMEERR);
|
||||
|
||||
switch ($labels->first()) {
|
||||
case '_binkp':
|
||||
$mailer = Mailer::where('name','BINKP')->singleOrFail();
|
||||
break;
|
||||
|
||||
case '_ifcico':
|
||||
$mailer = Mailer::where('name','EMSI')->singleOrFail();
|
||||
break;
|
||||
|
||||
default:
|
||||
return $this->reply(self::DNS_NAMEERR);
|
||||
}
|
||||
|
||||
$labels->shift(2);
|
||||
}
|
||||
|
||||
// First check that it is a query we can answer
|
||||
// First label should be p.. or f..
|
||||
if (! is_null($p=$this->parse('p',$labels->first())))
|
||||
$labels->shift();
|
||||
else
|
||||
$p = 0;
|
||||
|
||||
if (is_null($f=$this->parse('f',$labels->shift())))
|
||||
return $this->nameerr();
|
||||
// We'll assume f0
|
||||
if (! is_null($f=$this->parse('f',$labels->first())))
|
||||
$labels->shift();
|
||||
else
|
||||
$f = 0;
|
||||
|
||||
if (is_null($n=$this->parse('n',$labels->shift())))
|
||||
return $this->nameerr();
|
||||
@ -200,22 +230,51 @@ final class DNS extends BaseProtocol
|
||||
$ao = Address::findFTN(sprintf('%d:%d/%d.%d@%s',$z,$n,$f,$p,$d));
|
||||
|
||||
// Check we have the right record
|
||||
if ((! $ao) || (! $ao->system->address) || (($rootdn !== self::TLD) && ((! $ao->zone->domain->dnsdomain) || ($ao->zone->domain->dnsdomain !== $rootdn)))) {
|
||||
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();
|
||||
}
|
||||
|
||||
Log::info(sprintf('%s:= Returning [%s] for DNS query [%s]',self::LOGKEY,$ao->system->address,$ao->ftn));
|
||||
switch ($this->query->type) {
|
||||
case self::DNS_TYPE_SRV:
|
||||
Log::info(sprintf('%s:= Returning [%s] for DNS query [%s]',self::LOGKEY,$ao->system->address,$ao->ftn));
|
||||
|
||||
return $this->reply(
|
||||
self::DNS_NOERROR,
|
||||
[serialize($this->domain_split($ao->system->address)) => self::DNS_TYPE_CNAME]);
|
||||
if (($ao->system->address) && ($xx=$ao->system->mailers->where('id',$mailer->id)->pop())) {
|
||||
return $this->reply(
|
||||
self::DNS_NOERROR,
|
||||
[serialize([
|
||||
0, // priority
|
||||
1, // weight
|
||||
$xx->pivot->port,
|
||||
$this->domain_split($ao->system->address),
|
||||
]) => self::DNS_TYPE_SRV]);
|
||||
|
||||
} else {
|
||||
return $this->nodata();
|
||||
}
|
||||
|
||||
case self::DNS_TYPE_TXT:
|
||||
Log::info(sprintf('%s:= Returning [%s] for DNS query [%s]',self::LOGKEY,$ao->system->name,$ao->ftn));
|
||||
|
||||
return $this->reply(
|
||||
self::DNS_NOERROR,
|
||||
[serialize($ao->system->name) => self::DNS_TYPE_TXT]);
|
||||
|
||||
default:
|
||||
Log::info(sprintf('%s:= Returning [%s] for DNS query [%s]',self::LOGKEY,$ao->system->address,$ao->ftn));
|
||||
|
||||
return (! $ao->system->address)
|
||||
? $this->nodata()
|
||||
: $this->reply(
|
||||
self::DNS_NOERROR,
|
||||
[serialize($this->domain_split($ao->system->address)) => self::DNS_TYPE_CNAME]);
|
||||
}
|
||||
|
||||
// Other attributes return NOTIMPL
|
||||
default:
|
||||
Log::error(sprintf('%s:! We dont support DNS query types [%d]',self::LOGKEY,$this->query->type));
|
||||
|
||||
return $this->reply(self::DNS_NOTIMPLEMENTED);
|
||||
return $this->reply(self::DNS_NOTIMPLEMENTED,[],$this->soa());
|
||||
}
|
||||
}
|
||||
|
||||
@ -230,11 +289,36 @@ final class DNS extends BaseProtocol
|
||||
return pack('n',$offset | (3 << 14));
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a domain into a DNS domain string
|
||||
*
|
||||
* @param string $domain
|
||||
* @return string
|
||||
*/
|
||||
private function domain_split(string $domain): string
|
||||
{
|
||||
$a = '';
|
||||
|
||||
foreach (explode('.',$domain) as $item)
|
||||
$a .= pack('C',strlen($item)).$item;
|
||||
|
||||
$a .= "\x00";
|
||||
|
||||
return $a;
|
||||
}
|
||||
|
||||
private function nameerr(): int
|
||||
{
|
||||
Log::error(sprintf('%s:! DNS query for a resource we dont manage [%s]',self::LOGKEY,$this->query->domain));
|
||||
|
||||
return $this->reply(self::DNS_NAMEERR);
|
||||
return $this->reply(self::DNS_NAMEERR,[],$this->soa());
|
||||
}
|
||||
|
||||
private function nodata(): int
|
||||
{
|
||||
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());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -248,7 +332,7 @@ final class DNS extends BaseProtocol
|
||||
{
|
||||
$m = [];
|
||||
|
||||
return (preg_match('/^'.$prefix.'([0-9]+)+/',$label,$m) && ($m[1] <= DomainController::NUMBER_MAX))
|
||||
return (preg_match('/^'.$prefix.'([0-9]+)+/',$label,$m) && ($m[1] <= Address::ADDRESS_FIELD_MAX))
|
||||
? $m[1]
|
||||
: NULL;
|
||||
}
|
||||
@ -320,24 +404,6 @@ final class DNS extends BaseProtocol
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a domain into a DNS domain string
|
||||
*
|
||||
* @param string $domain
|
||||
* @return string
|
||||
*/
|
||||
private function domain_split(string $domain): string
|
||||
{
|
||||
$a = '';
|
||||
|
||||
foreach (explode('.',$domain) as $item)
|
||||
$a .= pack('C',strlen($item)).$item;
|
||||
|
||||
$a .= "\x00";
|
||||
|
||||
return $a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a DNS Resource Record
|
||||
*
|
||||
@ -378,10 +444,34 @@ final class DNS extends BaseProtocol
|
||||
$a .= pack('NNNNN',$ars[2],$ars[3],$ars[4],$ars[5],$ars[6]);
|
||||
|
||||
break;
|
||||
|
||||
case self::DNS_TYPE_SRV:
|
||||
$a .= pack('nnn',$ars[0],$ars[1],$ars[2]);
|
||||
$a .= $ars[3];
|
||||
|
||||
break;
|
||||
|
||||
case self::DNS_TYPE_TXT:
|
||||
$a .= pack('C',strlen($ars)).$ars;
|
||||
break;
|
||||
}
|
||||
|
||||
$reply .= pack('n',strlen($a)).$a;
|
||||
|
||||
return $reply;
|
||||
}
|
||||
|
||||
private function soa(): array
|
||||
{
|
||||
return
|
||||
[serialize([
|
||||
$this->domain_split(config('fido.dns_ns')),
|
||||
$this->domain_split(Str::replace('@','.',config('app.mail.mail_from','nobody@'.gethostname()))),
|
||||
1, // Serial
|
||||
self::DEFAULT_TTL, // Refresh
|
||||
self::DEFAULT_TTL, // Retry
|
||||
self::DEFAULT_TTL*7,// Expire
|
||||
self::DEFAULT_TTL // Minimum cache
|
||||
]) => self::DNS_TYPE_SOA];
|
||||
}
|
||||
}
|
@ -32,7 +32,8 @@ final class Query
|
||||
'arcount' => [0x05,'n',1], // Resource Records in the addition records section
|
||||
];
|
||||
|
||||
public function __construct(string $buf) {
|
||||
public function __construct(string $buf)
|
||||
{
|
||||
$this->buf = $buf;
|
||||
$rx_ptr = 0;
|
||||
|
||||
@ -49,12 +50,20 @@ final class Query
|
||||
$this->labels = collect();
|
||||
|
||||
while (($len=ord(substr($this->buf,$rx_ptr++,1))) !== 0x00) {
|
||||
$this->labels->push(substr($this->buf,$rx_ptr,$len));
|
||||
$this->labels->push(strtolower(substr($this->buf,$rx_ptr,$len)));
|
||||
$rx_ptr += $len;
|
||||
}
|
||||
|
||||
// Get the query type/class
|
||||
$result = unpack('ntype/nclass',substr($this->buf,$rx_ptr,4));
|
||||
try {
|
||||
$result = unpack('ntype/nclass',substr($this->buf,$rx_ptr,4));
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error(sprintf('%s:! Unpack failed: Buffer: [%s] (%d), RXPTR [%d]',self::LOGKEY,hex_dump($this->buf),strlen($this->buf),$rx_ptr));
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$rx_ptr += 4;
|
||||
$this->type = $result['type'];
|
||||
$this->class = $result['class'];
|
||||
@ -65,17 +74,16 @@ final class Query
|
||||
if ($this->arcount) {
|
||||
// Additional records, EDNS: https://datatracker.ietf.org/doc/html/rfc6891
|
||||
if (($haystack = strstr(substr($this->buf,$rx_ptr+1+10),"\x00",true)) !== FALSE) {
|
||||
Log::error(sprintf('%s:! DNS additional record format error?',self::LOGKEY));
|
||||
// @todo catch this
|
||||
Log::error(sprintf('%s:! DNS additional record format error?',self::LOGKEY),['buf'=>hex_dump($this->buf)]);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->additional = new RR(substr($this->buf,$rx_ptr,(strlen($haystack) === 0) ? NULL : strlen($haystack)));
|
||||
$rx_ptr += $this->additional->length;
|
||||
}
|
||||
|
||||
if (strlen($this->buf) !== $rx_ptr) {
|
||||
dd(['query remaining'=>strlen($this->buf)-$rx_ptr,'hex'=>hex_dump(substr($this->buf,$rx_ptr))]);
|
||||
}
|
||||
if (strlen($this->buf) !== $rx_ptr)
|
||||
throw new \Exception(sprintf('! DNS Buffer still has [%d]: %s',strlen($this->buf)-$rx_ptr,hex_dump(substr($this->buf,$rx_ptr))));
|
||||
}
|
||||
|
||||
public function __get($key)
|
||||
@ -96,7 +104,8 @@ final class Query
|
||||
}
|
||||
}
|
||||
|
||||
public static function header_len() {
|
||||
public static function header_len()
|
||||
{
|
||||
return collect(self::header)->sum(function($item) { return $item[2]*2; });
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ namespace App\Classes\Protocol\DNS;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
use App\Classes\Protocol\DNS;
|
||||
|
||||
@ -26,8 +27,15 @@ final class RR
|
||||
$domain = strstr($buf,"\x00",TRUE);
|
||||
$i += strlen($domain)+1;
|
||||
|
||||
$this->type = Arr::get(unpack('n',substr($buf,$i,2)),1);
|
||||
$this->class = Arr::get(unpack('n',substr($buf,$i+2,2)),1);
|
||||
try {
|
||||
$this->type = Arr::get(unpack('n',substr($buf,$i,2)),1);
|
||||
$this->class = Arr::get(unpack('n',substr($buf,$i+2,2)),1);
|
||||
|
||||
} catch (\ErrorException $e) {
|
||||
Log::error(sprintf('%s:! Error unpacking buffer [%s]',self::LOGKEY,$buf),['buf'=>hex_dump($buf)]);
|
||||
return;
|
||||
}
|
||||
|
||||
$i += 4;
|
||||
|
||||
switch ($this->type) {
|
||||
|
@ -3,13 +3,13 @@
|
||||
namespace App\Classes\Protocol;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
use App\Classes\Protocol as BaseProtocol;
|
||||
use App\Classes\Sock\SocketClient;
|
||||
use App\Classes\Sock\SocketException;
|
||||
use App\Models\{Address,Setup};
|
||||
use App\Exceptions\InvalidFTNException;
|
||||
use App\Models\{Address,Mailer,Setup};
|
||||
use App\Interfaces\CRC as CRCInterface;
|
||||
use App\Interfaces\Zmodem as ZmodemInterface;
|
||||
use App\Traits\CRC as CRCTrait;
|
||||
@ -88,7 +88,6 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
|
||||
* @param SocketClient $client
|
||||
* @return int|null
|
||||
* @throws SocketException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function onConnect(SocketClient $client): ?int
|
||||
{
|
||||
@ -96,8 +95,7 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
|
||||
if (! parent::onConnect($client)) {
|
||||
Log::withContext(['pid'=>getmypid()]);
|
||||
|
||||
// @todo Can this be SESSION_EMSI? if so, set an object class value that in EMSI of SESSION_EMSI, and move this method to the parent class
|
||||
$this->session(self::SESSION_AUTO,$client,(new Address));
|
||||
$this->session(Mailer::where('name','EMSI')->singleOrFail(),$client,(new Address));
|
||||
$this->client->close();
|
||||
exit(0);
|
||||
}
|
||||
@ -108,7 +106,7 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
|
||||
/**
|
||||
* Send our welcome banner
|
||||
*
|
||||
* @throws Exception
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function emsi_banner(): void
|
||||
{
|
||||
@ -123,7 +121,7 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
|
||||
* Create the EMSI_DAT
|
||||
*
|
||||
* @return string
|
||||
* @throws Exception
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function emsi_makedat(): string
|
||||
{
|
||||
@ -273,7 +271,7 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
|
||||
*
|
||||
* @param string $str
|
||||
* @return int
|
||||
* @throws Exception
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function emsi_parsedat(string $str): int
|
||||
{
|
||||
@ -321,17 +319,38 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
|
||||
Log::debug(sprintf('%s: - Parsing AKA [%s]',self::LOGKEY,$rem_aka));
|
||||
|
||||
try {
|
||||
if (! ($o = Address::findFTN($rem_aka))) {
|
||||
if (! ($o = Address::findFTN($rem_aka,TRUE))) {
|
||||
Log::debug(sprintf('%s: ? AKA is UNKNOWN [%s]',self::LOGKEY,$rem_aka));
|
||||
|
||||
$this->node->ftn_other = $rem_aka;
|
||||
continue;
|
||||
|
||||
// If we only present limited AKAs dont validate password against akas outside of the domains we present
|
||||
} elseif (is_null(our_address($o))) {
|
||||
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;
|
||||
|
||||
} elseif (! $o->active) {
|
||||
Log::alert(sprintf('%s:/ AKA is not active [%s] - ignoring',self::LOGKEY,$rem_aka));
|
||||
|
||||
$this->node->ftn_other = $rem_aka;
|
||||
continue;
|
||||
|
||||
} else {
|
||||
Log::info(sprintf('%s:- Got AKA [%s]',self::LOGKEY,$rem_aka));
|
||||
}
|
||||
|
||||
} catch (Exception) {
|
||||
Log::error(sprintf('%s: ! AKA is INVALID [%s]',self::LOGKEY,$rem_aka));
|
||||
} catch (InvalidFTNException $e) {
|
||||
Log::error(sprintf('%s:! AKA is INVALID [%s] (%s), ignoring',self::LOGKEY,$rem_aka,$e->getMessage()));
|
||||
|
||||
continue;
|
||||
|
||||
} catch (\Exception) {
|
||||
Log::error(sprintf('%s: ! AKA is INVALID [%s]',self::LOGKEY,$rem_aka));
|
||||
|
||||
return self::S_FAILURE|self::S_ADDTRY;
|
||||
}
|
||||
|
||||
// Check if the remote has our AKA
|
||||
@ -504,7 +523,7 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
|
||||
* STEP 2A, RECEIVE EMSI HANDSHAKE
|
||||
*
|
||||
* @throws SocketException
|
||||
* @throws Exception
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function emsi_recv(int $mode): int
|
||||
{
|
||||
@ -683,7 +702,7 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
|
||||
* STEP 2B, TRANSMIT EMSI HANDSHAKE
|
||||
*
|
||||
* @throws SocketException
|
||||
* @throws Exception
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function emsi_send(): int
|
||||
{
|
||||
@ -822,7 +841,7 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
|
||||
* STEP 1, EMSI INIT
|
||||
*
|
||||
* @throws SocketException
|
||||
* @throws Exception
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function protocol_init(): int
|
||||
{
|
||||
@ -969,13 +988,14 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
|
||||
* Setup our EMSI session
|
||||
*
|
||||
* @return int
|
||||
* @throws Exception
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function protocol_session(): int
|
||||
protected function protocol_session(bool $force_queue=FALSE): int
|
||||
{
|
||||
// @todo introduce emsi_init() to perform the job of protocol_init. Only needs to be done when we originate a session
|
||||
|
||||
Log::debug(sprintf('%s:+ Starting EMSI Protocol SESSION',self::LOGKEY));
|
||||
$this->force_queue = $force_queue;
|
||||
|
||||
$was_req = 0;
|
||||
$got_req = 0;
|
||||
@ -1184,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)->zmodem_receive($this->client,$zap,$this->recv,$this->node->address);
|
||||
$rc = (new Zmodem)->zmodem_receive($this->client,$zap,$this->recv,$this->node->address,$this->force_queue);
|
||||
|
||||
return ($rc === self::RCDO || $rc === self::ERROR);
|
||||
}
|
||||
@ -1194,7 +1214,7 @@ final class EMSI extends BaseProtocol implements CRCInterface,ZmodemInterface
|
||||
*
|
||||
* @param int $zap
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function wazoosend(int $zap): bool
|
||||
{
|
||||
|
@ -10,7 +10,7 @@ use App\Classes\File\{Receive,Send};
|
||||
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;
|
||||
|
||||
/**
|
||||
@ -213,7 +213,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
if (! parent::onConnect($client)) {
|
||||
Log::withContext(['pid'=>getmypid()]);
|
||||
|
||||
$this->session(self::SESSION_ZMODEM,$client);
|
||||
$this->session(Mailer::where('name','ZMODEM')->singleOrFail(),$client);
|
||||
$this->client->close();
|
||||
|
||||
Log::info(sprintf('%s:= onConnect - Connection closed [%s]',self::LOGKEY,$client->address_remote));
|
||||
@ -273,10 +273,11 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
/**
|
||||
* Setup our ZMODEM session
|
||||
*
|
||||
* @param bool $force_queue Not used here
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function protocol_session(): int
|
||||
public function protocol_session(bool $force_queue=FALSE): int
|
||||
{
|
||||
$proto = $this->originate ? $this->node->optionGet(self::P_MASK) : $this->optionGet(self::P_MASK);
|
||||
|
||||
@ -301,7 +302,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
* @param int $canzap
|
||||
* @return int
|
||||
*/
|
||||
public function zmodem_receive(SocketClient $client,int $canzap,Receive $recv,Address $ao): int
|
||||
public function zmodem_receive(SocketClient $client,int $canzap,Receive $recv,Address $ao,bool $force_queue=FALSE): int
|
||||
{
|
||||
Log::debug(sprintf('%s:+ Starting ZModem Receive [%d]',self::LOGKEY,$canzap));
|
||||
|
||||
@ -321,7 +322,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
return self::OK;
|
||||
|
||||
case self::ZFILE:
|
||||
Log::debug(sprintf('%s: = zmodem_receive ZFILE after INIT',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:= zmodem_receive ZFILE after INIT',self::LOGKEY));
|
||||
break;
|
||||
|
||||
default:
|
||||
@ -343,46 +344,46 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
|
||||
case self::ZFILE:
|
||||
if (! $this->recv->togo_count) {
|
||||
Log::error(sprintf('%s: ! zmodem_receive No files to get?',self::LOGKEY));
|
||||
Log::error(sprintf('%s:! zmodem_receive No files to get?',self::LOGKEY));
|
||||
|
||||
$frame = self::ZSKIP;
|
||||
|
||||
} else {
|
||||
switch ($this->recv->open()) {
|
||||
case self::FOP_SKIP:
|
||||
Log::info(sprintf('%s: = zmodem_receive Skip this file [%s]',self::LOGKEY,$this->recv->nameas));
|
||||
Log::info(sprintf('%s:= zmodem_receive Skip this file [%s]',self::LOGKEY,$this->recv->nameas));
|
||||
$frame = self::ZSKIP;
|
||||
|
||||
break;
|
||||
|
||||
case self::FOP_SUSPEND:
|
||||
Log::info(sprintf('%s: = zmodem_receive Suspend this file [%s]',self::LOGKEY,$this->recv->nameas));
|
||||
Log::info(sprintf('%s:= zmodem_receive Suspend this file [%s]',self::LOGKEY,$this->recv->nameas));
|
||||
$frame = self::ZFERR;
|
||||
|
||||
break;
|
||||
|
||||
case self::FOP_CONT:
|
||||
case self::FOP_OK:
|
||||
Log::info(sprintf('%s: = zmodem_receive Receving [%s] from [%d]',self::LOGKEY,$this->recv->nameas,$this->recv->pos));
|
||||
Log::info(sprintf('%s:= zmodem_receive Receiving [%s] from [%d]',self::LOGKEY,$this->recv->nameas,$this->recv->pos));
|
||||
$frame = self::ZRINIT;
|
||||
|
||||
switch (($rc=$this->ls_zrecvfile($recv->pos))) {
|
||||
case self::ZFERR:
|
||||
Log::debug(sprintf('%s: = zmodem_receive ZFERR',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:= zmodem_receive ZFERR',self::LOGKEY));
|
||||
$this->recv->close();
|
||||
$frame = self::ZFERR;
|
||||
|
||||
break;
|
||||
|
||||
case self::ZSKIP:
|
||||
Log::debug(sprintf('%s: = zmodem_receive ZSKIP',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:= zmodem_receive ZSKIP',self::LOGKEY));
|
||||
$this->recv->close();
|
||||
$frame = self::ZSKIP;
|
||||
|
||||
break;
|
||||
|
||||
case self::OK:
|
||||
Log::debug(sprintf('%s: = zmodem_receive OK',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:= zmodem_receive OK',self::LOGKEY));
|
||||
$this->recv->close();
|
||||
|
||||
break;
|
||||
@ -459,7 +460,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
break;
|
||||
|
||||
default:
|
||||
Log::error(sprintf('%s: ? zmodem_senddone Something strange [%d]',self::LOGKEY,$rc));
|
||||
Log::error(sprintf('%s:? zmodem_senddone Something strange [%d]',self::LOGKEY,$rc));
|
||||
|
||||
if ($rc < 0)
|
||||
return $rc;
|
||||
@ -489,7 +490,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
if (($rc=$this->ls_zinitsender(self::LSZ_WINDOW,'')) < 0)
|
||||
return $rc;
|
||||
|
||||
Log::debug(sprintf('%s: - ZMODEM Link Options %d/%d, %s%s%s%s',
|
||||
Log::debug(sprintf('%s:- ZMODEM Link Options %d/%d, %s%s%s%s',
|
||||
self::LOGKEY,
|
||||
$this->ls_MaxBlockSize,
|
||||
$this->ls_txWinSize,
|
||||
@ -536,7 +537,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
return $rc;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error(sprintf('%s: ! Error [%s]',self::LOGKEY,$e->getMessage()));
|
||||
Log::error(sprintf('%s:! Error [%s]',self::LOGKEY,$e->getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -878,12 +879,12 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
case self::XON|0x80:
|
||||
case self::XOFF|0x80:
|
||||
if (static::DEBUG)
|
||||
Log::debug(sprintf('%s: - ls_zdonereceiver XON/XOFF, skip it',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ls_zdonereceiver XON/XOFF, skip it',self::LOGKEY));
|
||||
break;
|
||||
|
||||
case self::ZPAD:
|
||||
if (static::DEBUG)
|
||||
Log::debug(sprintf('%s: - ls_zdonereceiver ZPAD',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ls_zdonereceiver ZPAD',self::LOGKEY));
|
||||
|
||||
if (($rc=$this->ls_zrecvhdr($this->ls_rxHdr,$this->ls_HeaderTimeout)) < 0)
|
||||
return $rc;
|
||||
@ -948,7 +949,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
switch ($rc=$this->ls_zrecvhdr($this->ls_rxHdr,$this->ls_HeaderTimeout)) {
|
||||
/* Ok, We got RINIT! */
|
||||
case self::ZRINIT:
|
||||
Log::debug(sprintf('%s: - ls_zinitsender ZRINIT',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ls_zinitsender ZRINIT',self::LOGKEY));
|
||||
|
||||
$this->rxOptions= (($this->ls_rxHdr[self::LSZ_F1]&0xff)<<8)|($this->ls_rxHdr[self::LSZ_F0]&0xff);
|
||||
|
||||
@ -979,7 +980,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
if ($window && (! $this->ls_txWinSize || ($this->ls_txWinSize > $window)))
|
||||
$this->ls_txWinSize = $window;
|
||||
|
||||
Log::debug(sprintf('%s: - ls_zinitsender ZRINIT OK - effproto [%08x], winsize [%d]',self::LOGKEY,$this->ls_Protocol,$this->ls_txWinSize));
|
||||
Log::debug(sprintf('%s:- ls_zinitsender ZRINIT OK - effproto [%08x], winsize [%d]',self::LOGKEY,$this->ls_Protocol,$this->ls_txWinSize));
|
||||
|
||||
/* Ok, now we could calculate real max frame size and initial block size */
|
||||
if ($this->ls_txWinSize && $this->ls_MaxBlockSize>$this->ls_txWinSize) {
|
||||
@ -1013,7 +1014,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
if ($this->ls_txCurBlockSize>$this->ls_MaxBlockSize)
|
||||
$this->ls_txCurBlockSize=$this->ls_MaxBlockSize;
|
||||
|
||||
Log::debug(sprintf('%s: - ls_zinitsender ZRINIT OK - block sizes Max [%d] Current [%d]',self::LOGKEY,$this->ls_MaxBlockSize,$this->ls_txCurBlockSize));
|
||||
Log::debug(sprintf('%s:- ls_zinitsender ZRINIT OK - block sizes Max [%d] Current [%d]',self::LOGKEY,$this->ls_MaxBlockSize,$this->ls_txCurBlockSize));
|
||||
|
||||
/* Send ZSINIT, if we need it */
|
||||
if ($attstr || (! ($this->rxOptions&self::LSZ_RXWNTESCCTL) && ($this->ls_Protocol&self::LSZ_OPTESCAPEALL)))
|
||||
@ -1024,7 +1025,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
|
||||
/* Return number to peer, he is paranoid */
|
||||
case self::ZCHALLENGE:
|
||||
Log::debug(sprintf('%s: - ls_zinitsender CHALLENGE',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ls_zinitsender CHALLENGE',self::LOGKEY));
|
||||
|
||||
if (($rc=$this->ls_zsendhhdr(ZACK,$this->ls_rxHdr)) < 0)
|
||||
return $rc;
|
||||
@ -1040,7 +1041,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
|
||||
/* ZFIN from previous session? Or may be real one? */
|
||||
case self::ZFIN:
|
||||
Log::debug(sprintf('%s: - ls_zinitsender ZFIN [%d]',self::LOGKEY,$zfins));
|
||||
Log::debug(sprintf('%s:- ls_zinitsender ZFIN [%d]',self::LOGKEY,$zfins));
|
||||
|
||||
if (++$zfins === self::LSZ_TRUSTZFINS)
|
||||
return self::ERROR;
|
||||
@ -1049,11 +1050,11 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
|
||||
/* Please, resend */
|
||||
case self::LSZ_BADCRC:
|
||||
Log::debug(sprintf('%s: - ls_zinitsender LSZ_BADCRC',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ls_zinitsender LSZ_BADCRC',self::LOGKEY));
|
||||
|
||||
/* We don't support it! */
|
||||
case self::ZCOMMAND:
|
||||
Log::debug(sprintf('%s: - ls_zinitsender ZCOMMAND',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ls_zinitsender ZCOMMAND',self::LOGKEY));
|
||||
$this->ls_zsendhhdr(ZNAK,$this->ls_storelong(0));
|
||||
|
||||
/* Abort this session -- we trust in ABORT! */
|
||||
@ -1062,7 +1063,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
return self::ERROR;
|
||||
|
||||
default:
|
||||
Log::error(sprintf('%s: ? ls_zinitsender Something strange [%d]',self::LOGKEY,$rc));
|
||||
Log::error(sprintf('%s:? ls_zinitsender Something strange [%d]',self::LOGKEY,$rc));
|
||||
|
||||
if ($rc < 0)
|
||||
return $rc;
|
||||
@ -1095,7 +1096,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
|
||||
while ($rcvdata && (($c = $this->ls_readzdle($timeout)) >= 0)) {
|
||||
if (static::DEBUG)
|
||||
Log::debug(sprintf('%s: - ls_zrecvdata16 got [%x] (%c)',self::LOGKEY,$c,($c<31 ? 32 : $c)),['c'=>serialize($c)]);
|
||||
Log::debug(sprintf('%s:- ls_zrecvdata16 got [%x] (%c)',self::LOGKEY,$c,($c<31 ? 32 : $c)),['c'=>serialize($c)]);
|
||||
|
||||
if ($c < 256) {
|
||||
$data .= chr($c&0xff);
|
||||
@ -1141,7 +1142,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
$incrc = crc16($data.chr($frametype));
|
||||
|
||||
if (static::DEBUG)
|
||||
Log::debug(sprintf('%s: - ls_zrecvdata16 CRC%d got %08x - calculated %08x',self::LOGKEY,16,$incrc,$crc));
|
||||
Log::debug(sprintf('%s:- ls_zrecvdata16 CRC%d got %08x - calculated %08x',self::LOGKEY,16,$incrc,$crc));
|
||||
|
||||
if ($incrc != $crc)
|
||||
return self::LSZ_BADCRC;
|
||||
@ -1174,7 +1175,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
|
||||
while ($rcvdata && (($c=$this->ls_readzdle($timeout)) >= 0)) {
|
||||
if (static::DEBUG)
|
||||
Log::debug(sprintf('%s: - ls_zrecvdata32 got [%x] (%c)',self::LOGKEY,$c,($c<31 ? 32 : $c)),['c'=>serialize($c)]);
|
||||
Log::debug(sprintf('%s:- ls_zrecvdata32 got [%x] (%c)',self::LOGKEY,$c,($c<31 ? 32 : $c)),['c'=>serialize($c)]);
|
||||
|
||||
if ($c < 256) {
|
||||
$data .= chr($c&0xff);
|
||||
@ -1226,7 +1227,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
|
||||
$incrc = crc32($data.chr($frametype));
|
||||
if (static::DEBUG)
|
||||
Log::debug(sprintf('%s: - ls_zrecvdata32 CRC%d got %08x - calculated %08x',self::LOGKEY,32,$incrc,$crc));
|
||||
Log::debug(sprintf('%s:- ls_zrecvdata32 CRC%d got %08x - calculated %08x',self::LOGKEY,32,$incrc,$crc));
|
||||
|
||||
if ($incrc != $crc)
|
||||
return self::LSZ_BADCRC;
|
||||
@ -1266,7 +1267,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
$needzdata = 1;
|
||||
|
||||
case self::ZCRCG:
|
||||
Log::debug(sprintf('%s: - ls_zrecvfile ZCRC%s, [%d] bytes at [%d]',self::LOGKEY,($rc==self::ZCRCE ? 'E' : 'G'),$len,$rxpos));
|
||||
Log::debug(sprintf('%s:- ls_zrecvfile ZCRC%s, [%d] bytes at [%d]',self::LOGKEY,($rc==self::ZCRCE ? 'E' : 'G'),$len,$rxpos));
|
||||
|
||||
$rxpos += $len;
|
||||
|
||||
@ -1281,7 +1282,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
$needzdata = 1;
|
||||
|
||||
case self::ZCRCQ:
|
||||
Log::debug(sprintf('%s: - ls_zrecvfile ZCRC%s, [%d] bytes at [%d]',self::LOGKEY,($rc==self::ZCRCW ? 'W' : 'Q'),$len,$rxpos));
|
||||
Log::debug(sprintf('%s:- ls_zrecvfile ZCRC%s, [%d] bytes at [%d]',self::LOGKEY,($rc==self::ZCRCW ? 'W' : 'Q'),$len,$rxpos));
|
||||
|
||||
$rxpos += $len;
|
||||
|
||||
@ -1308,7 +1309,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
break;
|
||||
|
||||
default:
|
||||
Log::error(sprintf('%s: ? ls_zrecvfile Something strange [%d]',self::LOGKEY,$rc));
|
||||
Log::error(sprintf('%s:? ls_zrecvfile Something strange [%d]',self::LOGKEY,$rc));
|
||||
|
||||
if ($rc < 0)
|
||||
return $rc;
|
||||
@ -1325,13 +1326,13 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
|
||||
/* We need new position -- ZDATA (and may be ZEOF) */
|
||||
} else {
|
||||
Log::debug(sprintf('%s: - ls_zrecvfile Want ZDATA/ZEOF at [%d]',self::LOGKEY,$rxpos));
|
||||
Log::debug(sprintf('%s:- ls_zrecvfile Want ZDATA/ZEOF at [%d]',self::LOGKEY,$rxpos));
|
||||
|
||||
if (($rc=$this->ls_zrecvnewpos($rxpos,$newpos)) < 0)
|
||||
return $rc;
|
||||
|
||||
if ($newpos != $rxpos) {
|
||||
Log::error(sprintf('%s: - ls_zrecvfile Bad new position [%d] in [%d]',self::LOGKEY,$newpos,$rc));
|
||||
Log::error(sprintf('%s:- ls_zrecvfile Bad new position [%d] in [%d]',self::LOGKEY,$newpos,$rc));
|
||||
|
||||
if ($this->ls_rxAttnStr) {
|
||||
$this->client->buffer_add($this->ls_rxAttnStr);
|
||||
@ -1345,7 +1346,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
|
||||
} else {
|
||||
if ($rc === self::ZEOF) {
|
||||
Log::debug(sprintf('%s: - ls_zrecvfile ZEOF',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ls_zrecvfile ZEOF',self::LOGKEY));
|
||||
|
||||
if (($rc=$this->ls_zsendhhdr(self::ZRINIT,$this->ls_storelong(0))) < 0)
|
||||
return $rc;
|
||||
@ -1353,7 +1354,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
return self::OK;
|
||||
}
|
||||
|
||||
Log::debug(sprintf('%s: - ls_zrecvfile ZDATA',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ls_zrecvfile ZDATA',self::LOGKEY));
|
||||
$needzdata = 0;
|
||||
}
|
||||
}
|
||||
@ -1405,7 +1406,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
return $rc;
|
||||
}
|
||||
|
||||
Log::debug(sprintf('%s: - ZRINIT',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ZRINIT',self::LOGKEY));
|
||||
|
||||
$txHdr = [];
|
||||
$txHdr[self::LSZ_P0] = ($win&0xff);
|
||||
@ -1423,22 +1424,22 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
switch (($rc=$this->ls_zrecvhdr($this->ls_rxHdr,$this->ls_HeaderTimeout))) {
|
||||
/* Send ZRINIT again */
|
||||
case self::ZRQINIT:
|
||||
Log::debug(sprintf('%s: - ls_zrecvfinfo ZRQINIT',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ls_zrecvfinfo ZRQINIT',self::LOGKEY));
|
||||
/* We will trust in first ZFIN after ZRQINIT */
|
||||
$first = 1;
|
||||
|
||||
case self::ZNAK:
|
||||
Log::debug(sprintf('%s: - ls_zrecvfinfo ZNAK',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ls_zrecvfinfo ZNAK',self::LOGKEY));
|
||||
|
||||
case self::TIMEOUT:
|
||||
Log::debug(sprintf('%s: - ls_zrecvfinfo TIMEOUT',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ls_zrecvfinfo TIMEOUT',self::LOGKEY));
|
||||
$retransmit = 1;
|
||||
|
||||
break;
|
||||
|
||||
/* He want to set some options */
|
||||
case self::ZSINIT:
|
||||
Log::debug(sprintf('%s: - ls_zrecvfinfo ZSINIT',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ls_zrecvfinfo ZSINIT',self::LOGKEY));
|
||||
if (($rc=$this->ls_zrecvcrcw($this->rxbuf,$len)) < 0)
|
||||
return $rc;
|
||||
|
||||
@ -1466,7 +1467,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
|
||||
/* Ok, File started! */
|
||||
case self::ZFILE:
|
||||
Log::debug(sprintf('%s: - ls_zrecvfinfo ZFILE',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ls_zrecvfinfo ZFILE',self::LOGKEY));
|
||||
|
||||
if (($rc=$this->ls_zrecvcrcw($this->rxbuf,$len)) < 0)
|
||||
return $rc;
|
||||
@ -1485,11 +1486,11 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
$filesleft, // @todo Should track this
|
||||
$bytesleft) < 2)
|
||||
{
|
||||
Log::error(sprintf('%s: ! ls_zrecvfinfo File info is corrupted [%s]',self::LOGKEY,$this->rxbuf));
|
||||
Log::error(sprintf('%s:! ls_zrecvfinfo File info is corrupted [%s]',self::LOGKEY,$this->rxbuf));
|
||||
$filesleft = -1;
|
||||
|
||||
} else {
|
||||
$this->recv->new($file,$ao);
|
||||
$this->recv->new($file,$ao,$this->force_queue);
|
||||
}
|
||||
|
||||
return self::ZFILE;
|
||||
@ -1503,7 +1504,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
|
||||
/* ZFIN from previous session? Or may be real one? */
|
||||
case self::ZFIN:
|
||||
Log::debug(sprintf('%s: - ls_zrecvfinfo ZFIN [%d], first [%d]',self::LOGKEY,$zfins,$first));
|
||||
Log::debug(sprintf('%s:- ls_zrecvfinfo ZFIN [%d], first [%d]',self::LOGKEY,$zfins,$first));
|
||||
|
||||
if ($first || (++$zfins === self::LSZ_TRUSTZFINS))
|
||||
return self::ZFIN;
|
||||
@ -1516,7 +1517,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
return self::ZABORT;
|
||||
|
||||
case self::LSZ_BADCRC:
|
||||
Log::debug(sprintf('%s: - ls_zrecvfinfo BADCRC',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ls_zrecvfinfo BADCRC',self::LOGKEY));
|
||||
|
||||
$this->ls_zsendhhdr(self::ZNAK,$this->ls_storelong(0));
|
||||
$retransmit = 1;
|
||||
@ -1524,7 +1525,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
break;
|
||||
|
||||
default:
|
||||
Log::error(sprintf('%s: ? ls_zrecvfinfo Something strange [%d]',self::LOGKEY,$rc));
|
||||
Log::error(sprintf('%s:? ls_zrecvfinfo Something strange [%d]',self::LOGKEY,$rc));
|
||||
|
||||
if ($rc < 0)
|
||||
return $rc;
|
||||
@ -1601,7 +1602,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
}
|
||||
|
||||
if (static::DEBUG)
|
||||
Log::debug(sprintf('%s: - ls_zrecvhdr [%x] (%c)',self::LOGKEY,$c,$c));
|
||||
Log::debug(sprintf('%s:- ls_zrecvhdr [%x] (%c)',self::LOGKEY,$c,$c));
|
||||
|
||||
/* Here is error */
|
||||
if ($c < 0)
|
||||
@ -1716,7 +1717,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
|
||||
case self::rhBYTE:
|
||||
if (static::DEBUG)
|
||||
Log::debug(sprintf('%s: / ls_zrecvhdr [%02x] (%d)',self::LOGKEY,$c,$got));
|
||||
Log::debug(sprintf('%s:/ ls_zrecvhdr [%02x] (%d)',self::LOGKEY,$c,$got));
|
||||
|
||||
$hdr[$got] = $c;
|
||||
|
||||
@ -1729,7 +1730,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
|
||||
case self::rhCRC:
|
||||
if (static::DEBUG)
|
||||
Log::debug(sprintf('%s: %% ls_zrecvhdr [%02x] (%d|%d)',self::LOGKEY,$c,$crcgot+1,$got));
|
||||
Log::debug(sprintf('%s:%% ls_zrecvhdr [%02x] (%d|%d)',self::LOGKEY,$c,$crcgot+1,$got));
|
||||
|
||||
if ($crcl === 2) {
|
||||
$crc <<= 8;
|
||||
@ -1832,7 +1833,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
$state = self::rhInit;
|
||||
|
||||
if (static::DEBUG)
|
||||
Log::debug(sprintf('%s: - ls_zrecvhdr rhXON',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ls_zrecvhdr rhXON',self::LOGKEY));
|
||||
|
||||
switch ($c) {
|
||||
case self::ZPAD:
|
||||
@ -1877,17 +1878,17 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
return self::OK;
|
||||
|
||||
case self::LSZ_BADCRC:
|
||||
Log::debug(sprintf('%s: - ls_zrecvcrcw got BADCRC',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ls_zrecvcrcw got BADCRC',self::LOGKEY));
|
||||
$this->ls_zsendhhdr(self::ZNAK,$this->ls_storelong(0));
|
||||
|
||||
return 1;
|
||||
|
||||
case self::TIMEOUT:
|
||||
Log::debug(sprintf('%s: - ls_zrecvcrcw got TIMEOUT',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ls_zrecvcrcw got TIMEOUT',self::LOGKEY));
|
||||
break;
|
||||
|
||||
default:
|
||||
Log::error(sprintf('%s: ? ls_zrecvcrcw Something strange [%d]',self::LOGKEY,$rc));
|
||||
Log::error(sprintf('%s:? ls_zrecvcrcw Something strange [%d]',self::LOGKEY,$rc));
|
||||
|
||||
if ($rc < 0)
|
||||
return $rc;
|
||||
@ -1938,7 +1939,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
break;
|
||||
|
||||
default:
|
||||
Log::error(sprintf('%s: ? ls_zrecvnewpos Something strange [%d]',self::LOGKEY,$rc));
|
||||
Log::error(sprintf('%s:? ls_zrecvnewpos Something strange [%d]',self::LOGKEY,$rc));
|
||||
|
||||
if ($rc < 0)
|
||||
return $rc;
|
||||
@ -2007,7 +2008,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
|
||||
if ($this->ls_Protocol&self::LSZ_OPTCRC32) {
|
||||
if (static::DEBUG)
|
||||
Log::debug(sprintf('%s: - ls_zsenddata CRC32',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ls_zsenddata CRC32',self::LOGKEY));
|
||||
|
||||
for ($n=0;$n<strlen($data);$n++)
|
||||
$this->ls_sendchar(ord($data[$n]));
|
||||
@ -2036,7 +2037,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
|
||||
} else {
|
||||
if (static::DEBUG)
|
||||
Log::debug(sprintf('%s: - ls_zsenddata CRC16',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ls_zsenddata CRC16',self::LOGKEY));
|
||||
|
||||
for ($n=0;$n<strlen($data);$n++) {
|
||||
$this->ls_sendchar(ord($data[$n]));
|
||||
@ -2144,7 +2145,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
switch (($rc=$this->ls_zsendfinfo($send,$sernum,$send->pos,$fileleft,$bytesleft))) {
|
||||
/* Ok, It's OK! */
|
||||
case self::ZRPOS:
|
||||
Log::debug(sprintf('%s: - ls_zsendfile ZRPOS to [%d]',self::LOGKEY,$send->pos));
|
||||
Log::debug(sprintf('%s:- ls_zsendfile ZRPOS to [%d]',self::LOGKEY,$send->pos));
|
||||
break;
|
||||
|
||||
/* Skip it */
|
||||
@ -2153,13 +2154,13 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
// @todo Should ZFERR be next to ZABORT?
|
||||
case self::ZFERR:
|
||||
// @todo Mark the file as skipped
|
||||
Log::debug(sprintf('%s: - ls_zsendfile ZSKIP/ZFERR',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ls_zsendfile ZSKIP/ZFERR',self::LOGKEY));
|
||||
return $rc;
|
||||
|
||||
case self::ZABORT:
|
||||
/* Session is aborted */
|
||||
case self::ZFIN:
|
||||
Log::debug(sprintf('%s: - ls_zsendfile ZABORT/ZFIN',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ls_zsendfile ZABORT/ZFIN',self::LOGKEY));
|
||||
$this->ls_zsendhhdr(self::ZFIN,$this->ls_storelong(0));
|
||||
|
||||
return self::ERROR;
|
||||
@ -2185,7 +2186,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
/* We need to send ZDATA if previous frame was ZCRCW
|
||||
Also, frame will be ZCRCW, if it is after RPOS */
|
||||
if ($frame === self::ZCRCW) {
|
||||
Log::debug(sprintf('%s: - ls_zsendfile send ZDATA at [%d]',self::LOGKEY,$send->pos));
|
||||
Log::debug(sprintf('%s:- ls_zsendfile send ZDATA at [%d]',self::LOGKEY,$send->pos));
|
||||
|
||||
if (($rc=$this->ls_zsendbhdr(self::ZDATA,$this->ls_storelong($send->pos))) < 0)
|
||||
return $rc;
|
||||
@ -2243,11 +2244,11 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
switch (($rc=$this->ls_zrecvhdr($this->ls_rxHdr,$needack ? $this->ls_HeaderTimeout : 0))) { // @todo set timeout to 5 for debugging wtih POP
|
||||
/* They don't need this file */
|
||||
case self::ZSKIP:
|
||||
Log::debug(sprintf('%s: - ls_zsendfile ZSKIP',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ls_zsendfile ZSKIP',self::LOGKEY));
|
||||
|
||||
/* Problems occured -- suspend file */
|
||||
case self::ZFERR:
|
||||
Log::debug(sprintf('%s: - ls_zsendfile ZFERR',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ls_zsendfile ZFERR',self::LOGKEY));
|
||||
return $rc;
|
||||
|
||||
/* Ok, position ACK */
|
||||
@ -2255,18 +2256,18 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
$this->ls_txLastACK = $this->ls_fetchlong($this->ls_rxHdr);
|
||||
|
||||
if (static::DEBUG)
|
||||
Log::debug(sprintf('%s: - ls_zsendfile ZACK',self::LOGKEY),['ls_rxHdr'=>$this->ls_rxHdr,'ls_txLastACK'=>$this->ls_txLastACK,'ls_rxHdr'=>$this->ls_rxHdr]);
|
||||
Log::debug(sprintf('%s:- ls_zsendfile ZACK',self::LOGKEY),['ls_rxHdr'=>$this->ls_rxHdr,'ls_txLastACK'=>$this->ls_txLastACK,'ls_rxHdr'=>$this->ls_rxHdr]);
|
||||
|
||||
break;
|
||||
|
||||
/* Repos */
|
||||
case self::ZRPOS:
|
||||
Log::debug(sprintf('%s: - ZRPOS',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ZRPOS',self::LOGKEY));
|
||||
|
||||
if (($rc=$this->ls_zrpos($send,$this->ls_fetchlong($this->ls_rxHdr))) < 0)
|
||||
return $rc;
|
||||
|
||||
Log::debug(sprintf('%s: - ZRPOS [%d]',self::LOGKEY,$rc));
|
||||
Log::debug(sprintf('%s:- ZRPOS [%d]',self::LOGKEY,$rc));
|
||||
|
||||
/* Force to retransmit ZDATA */
|
||||
$frame = self::ZCRCW;
|
||||
@ -2291,7 +2292,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
break;
|
||||
|
||||
default:
|
||||
Log::error(sprintf('%s: ? ls_zsendfile Something strange [%d]',self::LOGKEY,$rc));
|
||||
Log::error(sprintf('%s:? ls_zsendfile Something strange [%d]',self::LOGKEY,$rc));
|
||||
|
||||
if ($rc < 0)
|
||||
return $rc;
|
||||
@ -2350,7 +2351,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
|
||||
/* ACK for data -- it lost! */
|
||||
case self::ZACK:
|
||||
Log::debug(sprintf('%s: - ls_zsendfile ZACK after EOF',self::LOGKEY));
|
||||
Log::debug(sprintf('%s:- ls_zsendfile ZACK after EOF',self::LOGKEY));
|
||||
$this->ls_txLastACK = $this->ls_fetchlong($this->ls_rxHdr);
|
||||
|
||||
break;
|
||||
@ -2370,7 +2371,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
break;
|
||||
|
||||
default:
|
||||
Log::error(sprintf('%s: ? ls_zsendfile Something strange after ZEOF [%d]',self::LOGKEY,$rc));
|
||||
Log::error(sprintf('%s:? ls_zsendfile Something strange after ZEOF [%d]',self::LOGKEY,$rc));
|
||||
|
||||
if ($rc < 0)
|
||||
return $rc;
|
||||
@ -2506,7 +2507,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
break;
|
||||
|
||||
default:
|
||||
Log::error(sprintf('%s: ? Something strange [%d]',self::LOGKEY,$rc));
|
||||
Log::error(sprintf('%s:? Something strange [%d]',self::LOGKEY,$rc));
|
||||
|
||||
if ($rc < 0)
|
||||
return $rc;
|
||||
@ -2628,7 +2629,7 @@ final class Zmodem extends Protocol implements CRCInterface,ZmodemInterface
|
||||
break;
|
||||
|
||||
default:
|
||||
Log::error(sprintf('%s: ? Something strange [%d]',self::LOGKEY,$rc));
|
||||
Log::error(sprintf('%s:? Something strange [%d]',self::LOGKEY,$rc));
|
||||
|
||||
if ($rc < 0)
|
||||
return $rc;
|
||||
|
@ -52,6 +52,99 @@ final class SocketClient {
|
||||
if ($this->type === SOCK_STREAM) {
|
||||
socket_getsockname($connection,$this->address_local,$this->port_local);
|
||||
socket_getpeername($connection,$this->address_remote,$this->port_remote);
|
||||
|
||||
// If HAPROXY is used, work get the clients address
|
||||
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 ($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');
|
||||
}
|
||||
|
||||
// Version/Command
|
||||
$vc = $this->read_ch(5);
|
||||
|
||||
if (($x=($vc>>4)&0x7) !== 2) {
|
||||
Log::error(sprintf('%s:! HAPROXY version [%d] is not handled',self::LOGKEY,$x));
|
||||
|
||||
throw new SocketException(SocketException::CANT_CONNECT,'Unknown HAPROXY version');
|
||||
}
|
||||
|
||||
switch ($x=($vc&0x7)) {
|
||||
// HAPROXY internal
|
||||
case 0:
|
||||
Log::debug(sprintf('%s:! HAPROXY internal health-check',self::LOGKEY));
|
||||
throw new SocketException(SocketException::CANT_CONNECT,'Healthcheck');
|
||||
|
||||
// PROXY connection
|
||||
case 1:
|
||||
break;
|
||||
|
||||
default:
|
||||
Log::error(sprintf('%s:! HAPROXY command [%d] is not handled',self::LOGKEY,$x));
|
||||
|
||||
throw new SocketException(SocketException::CANT_CONNECT,'Unknown HAPROXY command');
|
||||
}
|
||||
|
||||
// Protocol/Address Family
|
||||
$pa = $this->read_ch(5);
|
||||
$p = NULL;
|
||||
|
||||
switch ($x=($pa>>4)&0x7) {
|
||||
case 1: // AF_INET
|
||||
$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,
|
||||
Arr::get($dst_port,1),
|
||||
));
|
||||
}
|
||||
|
||||
Log::info(sprintf('%s:+ Connection host [%s] on port [%d] (%s)',self::LOGKEY,$this->address_remote,$this->port_remote,$this->type));
|
||||
}
|
||||
}
|
||||
@ -108,10 +201,16 @@ final class SocketClient {
|
||||
|
||||
$sort = collect(['AAAA','A']);
|
||||
|
||||
// We only look at AAAA/A records
|
||||
$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 (filter_var($address,FILTER_VALIDATE_IP))
|
||||
$resolved = collect([[
|
||||
(($x=filter_var($address,FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) ? 'ipv6' : 'ip')=>$address,
|
||||
'type'=>$x ? 'AAAA' : 'A'
|
||||
]]);
|
||||
else
|
||||
// We only look at AAAA/A records
|
||||
$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));
|
||||
@ -320,7 +419,7 @@ final class SocketClient {
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error(sprintf('%s: - socket_recv Exception [%s]',self::LOGKEY,$e->getMessage()));
|
||||
Log::error(sprintf('%s:! socket_recv Exception [%s]',self::LOGKEY,$e->getMessage()));
|
||||
|
||||
throw new SocketException($x=socket_last_error($this->connection),socket_strerror($x));
|
||||
}
|
||||
|
@ -128,8 +128,9 @@ final class SocketServer {
|
||||
try {
|
||||
$r = new SocketClient($accept);
|
||||
|
||||
} catch (\ErrorException $e) {
|
||||
} catch (\Exception $e) {
|
||||
Log::error(sprintf('%s:! Creating Socket client failed? [%s]',self::LOGKEY,$e->getMessage()));
|
||||
socket_close($accept);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
37
app/Console/Commands/AddressIdle.php
Normal file
37
app/Console/Commands/AddressIdle.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
use App\Jobs\AddressIdle as Job;
|
||||
use App\Models\{Address,Domain};
|
||||
|
||||
class AddressIdle extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'address:idle'
|
||||
.' {domain : Domain}'
|
||||
.' {--ftn= : Limit to specific address}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Find and mark nodes as hold/down/delist if idle';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$do = Domain::where('name',$this->argument('domain'))->singleOrFail();
|
||||
|
||||
return Job::dispatchSync($do,$this->option('ftn') ? Address::findFTN($this->option('ftn')) : NULL);
|
||||
}
|
||||
}
|
93
app/Console/Commands/Areafix/Rescan.php
Normal file
93
app/Console/Commands/Areafix/Rescan.php
Normal file
@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Areafix;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
use App\Models\{Address,Echoarea,Echomail};
|
||||
|
||||
class Rescan extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'areafix:rescan {ftn} {area} {days?}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Resend some echomail to a node';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*/
|
||||
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)
|
||||
throw new \Exception('FTN not found: '.$this->argument('ftn'));
|
||||
|
||||
// Check that the area belongs to the domain for the FTN
|
||||
if (! $this->argument('area'))
|
||||
throw new \Exception('Areaname is required');
|
||||
|
||||
$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));
|
||||
|
||||
// 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));
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
namespace App\Console\Commands\BBS;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
@ -13,7 +13,7 @@ class ANSIDecode extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'ansi:decode'
|
||||
protected $signature = 'bbs:ansi:decode'
|
||||
.' {file : ANS file to decode}';
|
||||
|
||||
/**
|
||||
@ -26,10 +26,12 @@ class ANSIDecode extends Command
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(): int
|
||||
{
|
||||
echo ANSI::ansi($this->argument('file'));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
namespace App\Console\Commands\BBS;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
@ -13,7 +13,7 @@ class ANSIEncode extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'ansi:encode'
|
||||
protected $signature = 'bbs:ansi:encode'
|
||||
.' {file : ANS file to encode}';
|
||||
|
||||
/**
|
||||
@ -26,9 +26,9 @@ class ANSIEncode extends Command
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(): int
|
||||
{
|
||||
foreach (ANSI::binary($this->argument('file')) as $line) {
|
||||
foreach (str_split(bin2hex($line),2) as $y)
|
||||
@ -36,5 +36,7 @@ class ANSIEncode extends Command
|
||||
|
||||
echo "\r";
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
use App\Classes\Sock\SocketException;
|
||||
use App\Classes\Sock\SocketServer;
|
||||
use App\Classes\Protocol\Zmodem as ZmodemClass;
|
||||
|
||||
class CommZmodemReceive extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'comm:zmodem:receive';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'ZMODEM receive';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
* @throws SocketException
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
Log::info('Listening for ZMODEM connections...');
|
||||
|
||||
$server = new SocketServer(60177,'0.0.0.0');
|
||||
$server->handler = [new ZmodemClass,'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());
|
||||
}
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
use App\Classes\Protocol;
|
||||
use App\Classes\Protocol\Zmodem as ZmodemClass;
|
||||
use App\Classes\Sock\SocketClient;
|
||||
|
||||
class CommZmodemSend extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'comm:zmodem:send {ip}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'ZMODEM send';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
Log::info('Call ZMODEM send');
|
||||
|
||||
[$address,$service_port] = explode(':',$this->argument('ip'),2);
|
||||
$client = SocketClient::create($address,$service_port);
|
||||
|
||||
$o = new ZmodemClass;
|
||||
$o->session(Protocol::SESSION_ZMODEM,$client);
|
||||
|
||||
Log::info(sprintf('Connection ended: %s',$client->address_remote),['m'=>__METHOD__]);
|
||||
}
|
||||
}
|
34
app/Console/Commands/Debug/AddressCheck.php
Normal file
34
app/Console/Commands/Debug/AddressCheck.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Debug;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
use App\Models\Address;
|
||||
|
||||
class AddressCheck extends Command
|
||||
{
|
||||
protected $signature = 'debug:address:check'
|
||||
.' {ftn : FTN}';
|
||||
|
||||
protected $description = 'Check the addresses we use for a node';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$o = Address::findFTN($this->argument('ftn'));
|
||||
|
||||
if (! $o) {
|
||||
$this->error(sprintf('Address: %s doesnt exist?',$this->argument('ftn')));
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$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("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(',')));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
50
app/Console/Commands/Debug/AddressCheckRole.php
Normal file
50
app/Console/Commands/Debug/AddressCheckRole.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Debug;
|
||||
|
||||
use App\Models\Address;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class AddressCheckRole extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'debug:address:check:role {--f|fix : Fix the role}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Check address roles and optionally fix';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
foreach (Address::withTrashed()->with(['zone.domain'])->cursor() as $o) {
|
||||
// Trim the role bit from role, since we now work out a role automatically.
|
||||
// @todo This doesnt work, because role_id returns back the overridden role, and thus would remove it
|
||||
if (($o->role & Address::NODE_ALL) === $o->role_id) {
|
||||
$o->role &= ~$o->role_id;
|
||||
|
||||
if ((! $o->role) || ($o->role === Address::NODE_UNKNOWN))
|
||||
$o->role = NULL;
|
||||
|
||||
if ($o->getDirty())
|
||||
if ($this->option('fix')) {
|
||||
$o->save();
|
||||
|
||||
} else {
|
||||
$this->warn(sprintf('Not changing [%s](%s) from [%d] to [%d]',$o->ftn,$o->role_name,$o->getOriginal('role'),$o->role));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
namespace App\Console\Commands\Debug;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
use App\Models\Address;
|
||||
use App\Models\{Address,System};
|
||||
|
||||
class AddressMerge extends Command
|
||||
{
|
||||
@ -15,7 +15,7 @@ class AddressMerge extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'address:merge'
|
||||
protected $signature = 'debug:address:merge'
|
||||
.' {src : Source Address}'
|
||||
.' {dst : Destination Address}'
|
||||
.' {--F|force : Force}'
|
||||
@ -34,79 +34,84 @@ class AddressMerge extends Command
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(): int
|
||||
{
|
||||
$src = Address::withTrashed()->findOrfail($this->argument('src'));
|
||||
$dst = Address::withTrashed()->findOrfail($this->argument('dst'));
|
||||
|
||||
if ((! $this->option('ignore')) && ($src->system_id !== $dst->system_id) && ($src->system->name !== 'Discovered System')) {
|
||||
if ((! $this->option('ignore')) && ($src->system_id !== $dst->system_id) && ($src->system->name !== System::default)) {
|
||||
$this->error(sprintf('FTN addresses are from different systems (%s/%s)',$src->system->name,$dst->system->name));
|
||||
exit(1);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ((! $this->option('force')) && ($src->ftn !== $dst->ftn)) {
|
||||
$this->error(sprintf('FTN addresses are not the same (%s:%s)',$src->ftn,$dst->ftn));
|
||||
exit(1);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($src->active) {
|
||||
$this->error(sprintf('Source [%s] is still active',$src->ftn));
|
||||
exit(1);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
// Find all echomail seenbys
|
||||
$x = DB::update('update echomail_seenby set address_id=? where address_id=?',[$dst->id,$src->id]);
|
||||
$x = DB::update('UPDATE echomail_seenby SET address_id=? WHERE address_id=?',[$dst->id,$src->id]);
|
||||
$this->info(sprintf('Updated [%d] echomail seenby records',$x));
|
||||
|
||||
// Find all echomail paths
|
||||
$x = DB::update('update echomail_path set address_id=? where address_id=?',[$dst->id,$src->id]);
|
||||
$x = DB::update('UPDATE echomail_path SET address_id=? WHERE address_id=?',[$dst->id,$src->id]);
|
||||
$this->info(sprintf('Updated [%d] echomail path records',$x));
|
||||
|
||||
// Find all echomails
|
||||
$x = DB::update('update echomails set fftn_id=? where fftn_id=?',[$dst->id,$src->id]);
|
||||
$x = DB::update('UPDATE echomails SET fftn_id=? WHERE fftn_id=?',[$dst->id,$src->id]);
|
||||
$this->info(sprintf('Updated [%d] echomail source records',$x));
|
||||
|
||||
// Find all netmails
|
||||
$x = DB::update('update netmails set fftn_id=? where fftn_id=?',[$dst->id,$src->id]);
|
||||
$x = DB::update('UPDATE netmails SET fftn_id=? WHERE fftn_id=?',[$dst->id,$src->id]);
|
||||
$this->info(sprintf('Updated [%d] netmail source records',$x));
|
||||
|
||||
// Find all netmails
|
||||
$x = DB::update('update netmails set tftn_id=? where tftn_id=?',[$dst->id,$src->id]);
|
||||
$x = DB::update('UPDATE netmails SET tftn_id=? WHERE tftn_id=?',[$dst->id,$src->id]);
|
||||
$this->info(sprintf('Updated [%d] netmail destination records',$x));
|
||||
|
||||
// Find all nodelist
|
||||
$x = DB::update('update address_nodelist set address_id=? where address_id=?',[$dst->id,$src->id]);
|
||||
$x = DB::update('UPDATE address_nodelist SET address_id=? WHERE address_id=?',[$dst->id,$src->id]);
|
||||
$this->info(sprintf('Updated [%d] nodelist records',$x));
|
||||
|
||||
// Find all file seenbys
|
||||
$x = DB::update('update file_seenby set address_id=? where address_id=?',[$dst->id,$src->id]);
|
||||
$x = DB::update('UPDATE file_seenby SET address_id=? WHERE address_id=?',[$dst->id,$src->id]);
|
||||
$this->info(sprintf('Updated [%d] file seenby records',$x));
|
||||
|
||||
// Find all files
|
||||
$x = DB::update('update files set fftn_id=? where fftn_id=?',[$dst->id,$src->id]);
|
||||
$x = DB::update('UPDATE files SET fftn_id=? WHERE fftn_id=?',[$dst->id,$src->id]);
|
||||
$this->info(sprintf('Updated [%d] file source records',$x));
|
||||
|
||||
// Resubscribe echoareas
|
||||
try {
|
||||
$x = DB::update('update address_echoarea set address_id=? where address_id=?',[$dst->id,$src->id]);
|
||||
$x = DB::update('UPDATE address_echoarea SET address_id=? WHERE address_id=?',[$dst->id,$src->id]);
|
||||
|
||||
} catch (QueryException $e) {
|
||||
DB::rollback();
|
||||
$this->error(sprintf('You may need to remove %s:%s (%d) from echoareas',$src->ftn,$src->system->name,$src->id));
|
||||
exit(1);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info(sprintf('Updated [%d] echomail subscription records',$x));
|
||||
|
||||
// Resubscribe fileareas
|
||||
try {
|
||||
$x = DB::update('update address_filearea set address_id=? where address_id=?',[$dst->id,$src->id]);
|
||||
$x = DB::update('UPDATE address_filearea SET address_id=? WHERE address_id=?',[$dst->id,$src->id]);
|
||||
} catch (QueryException $e) {
|
||||
DB::rollback();
|
||||
$this->error(sprintf('You may need to remove %s:%s (%d) from fileareas',$src->ftn,$src->system->name,$src->id));
|
||||
exit(1);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info(sprintf('Updated [%d] filearea subscription records',$x));
|
||||
@ -117,7 +122,7 @@ class AddressMerge extends Command
|
||||
|
||||
} else {
|
||||
if ($src->forceDelete()) {
|
||||
$this->alert(sprintf('%s deleted.', $src->ftn));
|
||||
$this->alert(sprintf('%s deleted.',$src->ftn));
|
||||
DB::commit();
|
||||
|
||||
} else {
|
||||
@ -126,6 +131,6 @@ class AddressMerge extends Command
|
||||
}
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
36
app/Console/Commands/Debug/DynamicItem.php
Normal file
36
app/Console/Commands/Debug/DynamicItem.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Debug;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
use App\Classes\File\Send;
|
||||
use App\Classes\File\Send\Dynamic;
|
||||
use App\Models\Address;
|
||||
use App\Models\Dynamic as DynamicModel;
|
||||
|
||||
class DynamicItem extends Command
|
||||
{
|
||||
protected $signature = 'debug:dynamic:item'
|
||||
.' {name : Dynamic Item}'
|
||||
.' {ftn : FTN Address}';
|
||||
|
||||
protected $description = 'Generate a dynamic item';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$do = DynamicModel::where('name',$this->argument('name'))->single();
|
||||
|
||||
if (! $do)
|
||||
throw new \Exception(sprintf('Dynamic Item [%s] doesnt exist?',$this->argument('name')));
|
||||
|
||||
$ao = Address::findFTN($this->argument('ftn'));
|
||||
|
||||
$d = new Dynamic($do,$ao,Send::T_FILE);
|
||||
|
||||
$d->open();
|
||||
echo $d->read($d->size);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
namespace App\Console\Commands\Debug;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
@ -14,7 +14,7 @@ class EchomailDump extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'echomail:dump {id}';
|
||||
protected $signature = 'debug:echomail:dump {id}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@ -26,10 +26,12 @@ class EchomailDump extends Command
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(): int
|
||||
{
|
||||
dump(Echomail::findOrFail($this->argument('id')));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
namespace App\Console\Commands\Debug;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
@ -15,7 +15,7 @@ class NetmailTest extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'netmail:test'
|
||||
protected $signature = 'debug:netmail:test'
|
||||
.' {ftn : FTN to send netmail to}';
|
||||
|
||||
/**
|
||||
@ -28,13 +28,15 @@ class NetmailTest extends Command
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function handle()
|
||||
public function handle():int
|
||||
{
|
||||
$ao = Address::findFTN($this->argument('ftn'));
|
||||
|
||||
Notification::route('netmail',$ao)->notify(new NetmailTestNotification());
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
62
app/Console/Commands/Debug/PacketAddress.php
Normal file
62
app/Console/Commands/Debug/PacketAddress.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Debug;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
use App\Models\{Echomail,Netmail};
|
||||
use App\Models\Address;
|
||||
|
||||
class PacketAddress extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'debug:packet:address'
|
||||
.' {ftn : FTN Address}'
|
||||
.' {type : Message Type}'
|
||||
.' {dbid : Message ID}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Generate a mail packet for a system';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$ao = Address::findFTN($this->argument('ftn'));
|
||||
|
||||
switch ($this->argument('type')) {
|
||||
case 'echomail':
|
||||
$o = new Echomail;
|
||||
break;
|
||||
|
||||
case 'netmail':
|
||||
$o = new Netmail;
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->error('Unknown type: '.$this->argument('type'));
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo hex_dump($ao
|
||||
->system
|
||||
->packet($ao)
|
||||
->mail($o->where('id',$this->argument('dbid'))->get())
|
||||
->generate()
|
||||
);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
67
app/Console/Commands/Debug/PacketDump.php
Normal file
67
app/Console/Commands/Debug/PacketDump.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Debug;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
use App\Models\Address;
|
||||
|
||||
class PacketDump extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'debug:packet:dump'.
|
||||
' {type : Type of packet, netmail|echomail }'.
|
||||
' {ftn : FTN}'.
|
||||
' {file? : filename}'.
|
||||
' {--dump : Dump packet}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Create an outgoing FTN packet';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$ao = Address::findFTN($this->argument('ftn'));
|
||||
|
||||
switch (strtolower($this->argument('type'))) {
|
||||
case 'netmail':
|
||||
$pkt = $ao->getNetmail();
|
||||
break;
|
||||
|
||||
case 'echomail':
|
||||
$pkt = $ao->getEchomail();
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->error('Unknown type: '.$this->argument('type'));
|
||||
throw new \Exception('Unknown type: '.$this->argument('type'));
|
||||
}
|
||||
|
||||
if ($this->option('dump')) {
|
||||
$this->info('Item Name:'.$pkt->name);
|
||||
$this->info('Item Type:'.get_class($pkt));
|
||||
$this->info('Dump:');
|
||||
echo hex_dump($pkt);
|
||||
|
||||
} else {
|
||||
$f = fopen($this->argument('file'),'w+');
|
||||
fputs($f,(string)$pkt);
|
||||
fclose($f);
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
namespace App\Console\Commands\Debug;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
@ -15,7 +15,7 @@ class SendTestEmail extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'email:test {id}';
|
||||
protected $signature = 'debug:email:test {id}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@ -27,13 +27,15 @@ class SendTestEmail extends Command
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(): int
|
||||
{
|
||||
$uo = User::find($this->argument('id'));
|
||||
|
||||
Mail::to($uo->email)
|
||||
->send(new MailTest($uo));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
47
app/Console/Commands/Debug/ZoneCheck.php
Normal file
47
app/Console/Commands/Debug/ZoneCheck.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Debug;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
use App\Models\Domain;
|
||||
|
||||
class ZoneCheck extends Command
|
||||
{
|
||||
protected $signature = 'debug:zone:check'
|
||||
.' {domain : Domain Name}'
|
||||
.' {--Z|zone= : Zone}';
|
||||
|
||||
protected $description = 'Check that the addresses in a zone are configured correctly';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$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))
|
||||
continue;
|
||||
|
||||
$this->warn('Zone: '.$zo->zone_id);
|
||||
$this->info(sprintf('- Our address(es): %s',our_address($do)->pluck('ftn4d')->join(',')));
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
@ -30,9 +30,9 @@ class EchoareaImport extends Command
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(): int
|
||||
{
|
||||
$do = Domain::where('name',strtolower($this->argument('domain')))->singleOrFail();
|
||||
return Job::dispatchSync($this->argument('file'),$do,$this->option('prefix'),$this->option('unlink'));
|
||||
|
40
app/Console/Commands/FileareaImport.php
Normal file
40
app/Console/Commands/FileareaImport.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
use App\Models\Domain;
|
||||
use App\Jobs\FileareaImport as Job;
|
||||
|
||||
class FileareaImport extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'filearea:import'
|
||||
.' {file : NA File}'
|
||||
.' {domain : Domain}'
|
||||
.' {--P|prefix= : Add prefix to description}'
|
||||
.' {--U|unlink : Delete file after import}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Import Filearea';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$do = Domain::where('name',strtolower($this->argument('domain')))->singleOrFail();
|
||||
return Job::dispatchSync($this->argument('file'),$do,$this->option('prefix'),$this->option('unlink'));
|
||||
}
|
||||
}
|
90
app/Console/Commands/Filefix/Rescan.php
Normal file
90
app/Console/Commands/Filefix/Rescan.php
Normal file
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands\Filefix;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
use App\Models\{Address,Filearea,File};
|
||||
|
||||
class Rescan extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'filefix:rescan {ftn} {area} {file?}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Resend some files to a node';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$ao = Address::findFtn($this->argument('ftn'));
|
||||
|
||||
if (! $ao)
|
||||
throw new \Exception('FTN not found: '.$this->argument('ftn'));
|
||||
|
||||
// Check that the area belongs to the domain for the FTN
|
||||
if (! $this->argument('area'))
|
||||
throw new \Exception('Areaname is required');
|
||||
|
||||
$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));
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
@ -27,7 +27,7 @@ class FilesList extends Command
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(): int
|
||||
{
|
||||
$this->table([
|
||||
'files.id' => 'ID',
|
||||
@ -37,6 +37,6 @@ class FilesList extends Command
|
||||
->join('fileareas',['fileareas.id'=>'files.filearea_id'])
|
||||
->cursor());
|
||||
|
||||
return Command::SUCCESS;
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Database\Seeders\InitialSetupSeeder;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class InitialSetup extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'initial:setup';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Initial Setup of DB';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
Artisan::call('db:seed',['class'=>InitialSetupSeeder::class]);
|
||||
}
|
||||
}
|
@ -25,7 +25,7 @@ class JobList extends Command
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(): int
|
||||
{
|
||||
$lastq = NULL;
|
||||
|
||||
@ -35,16 +35,17 @@ class JobList extends Command
|
||||
$lastq = $o->queue;
|
||||
}
|
||||
|
||||
$this->info(sprintf('%s-%d: %s[%s] - %d tries (Created: %s,Timeout: %s,Next: %s)',
|
||||
$this->info(sprintf('%s-%d: %s[%s] - %d/%d tries [Next:%s]%s',
|
||||
$o->uuid,
|
||||
$o->id,
|
||||
$o->display_name,
|
||||
$o->command->subject,
|
||||
$o->attempts,
|
||||
$o->created_at,
|
||||
$o->retry_until ?: '-',
|
||||
$o->reserved_at ?: '-',
|
||||
$o->command->jobname,
|
||||
$o->attempts,$o->maxTries,
|
||||
$o->available_at ?: '-',
|
||||
$o->attempts ? sprintf(' (Created:%s)',$o->created_at) : ''
|
||||
));
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
79
app/Console/Commands/MailList.php
Normal file
79
app/Console/Commands/MailList.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
use App\Models\Address;
|
||||
|
||||
class MailList extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'mail:list {ftn : FTN address}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'List mail waiting for a node';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$ao = Address::findFTN($this->argument('ftn'),TRUE);
|
||||
|
||||
if (! $ao) {
|
||||
$this->error(sprintf('%s not found?',$this->argument('ftn')));
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info('Netmail');
|
||||
$this->table([
|
||||
'id' => 'ID',
|
||||
'msgid' => 'MSGID',
|
||||
'from' => 'FROM',
|
||||
'to' => 'TO',
|
||||
'subject' => 'SUBJECT',
|
||||
],$ao->netmailWaiting()->map(function($item) {
|
||||
return [
|
||||
'id'=>$item->id,
|
||||
'msgid'=>$item->msgid,
|
||||
'from'=>$item->from,
|
||||
'to'=>$item->to,
|
||||
'subject'=>$item->subject,
|
||||
];
|
||||
}));
|
||||
|
||||
$this->info('Echomail');
|
||||
$this->table([
|
||||
'id' => 'ID',
|
||||
'msgid' => 'MSGID',
|
||||
'from' => 'FROM',
|
||||
'to' => 'TO',
|
||||
'subject' => 'SUBJECT',
|
||||
'area' => 'AREA',
|
||||
],$ao->echomailWaiting()->map(function($item) {
|
||||
return [
|
||||
'id'=>$item->id,
|
||||
'msgid'=>$item->msgid,
|
||||
'from'=>$item->from,
|
||||
'to'=>$item->to,
|
||||
'subject'=>$item->subject,
|
||||
'area'=>$item->echoarea->name,
|
||||
];
|
||||
}));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
@ -27,7 +27,7 @@ class MailSend extends Command
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(): int
|
||||
{
|
||||
switch ($this->option('type')) {
|
||||
case 'crash':
|
||||
@ -48,5 +48,7 @@ class MailSend extends Command
|
||||
default:
|
||||
$this->error('Specify -T crash, normal or all');
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ class NodelistImport extends Command
|
||||
protected $signature = 'nodelist:import'
|
||||
.' {file : File ID | filename}'
|
||||
.' {domain? : Domain Name}'
|
||||
.' {--I|ignorecrc : Ignore the CRC}'
|
||||
.' {--D|delete : Delete old data for the date}'
|
||||
.' {--U|unlink : Delete file after import}'
|
||||
.' {--T|test : Dry run}';
|
||||
@ -31,16 +32,26 @@ class NodelistImport extends Command
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
public function handle():int
|
||||
{
|
||||
return Job::dispatchSync(
|
||||
is_numeric($x=$this->argument('file')) ? File::findOrFail($x) : $x,
|
||||
$this->argument('domain'),
|
||||
$this->option('delete'),
|
||||
$this->option('unlink'),
|
||||
$this->option('test')
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -3,10 +3,11 @@
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
use App\Classes\File;
|
||||
use App\Classes\FTN\Packet;
|
||||
use App\Models\System;
|
||||
use App\Models\{Address,Echomail};
|
||||
|
||||
class PacketInfo extends Command
|
||||
{
|
||||
@ -17,7 +18,7 @@ class PacketInfo extends Command
|
||||
*/
|
||||
protected $signature = 'packet:info'
|
||||
.' {file : Packet to process}'
|
||||
.' {system? : System the packet is from}';
|
||||
.' {ftn? : FTN the packet is from}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@ -29,41 +30,63 @@ class PacketInfo extends Command
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
* @throws \App\Classes\FTN\InvalidPacketException
|
||||
* @return int
|
||||
* @throws \App\Exceptions\InvalidPacketException
|
||||
*/
|
||||
public function handle()
|
||||
public function handle():int
|
||||
{
|
||||
$f = new File($this->argument('file'));
|
||||
$s = $this->argument('system') ? System::where('name',$this->argument('system'))->singleOrFail() : NULL;
|
||||
$fs = Storage::disk(config('fido.local_disk'));
|
||||
$rel_name = sprintf('%s/%s',config('fido.dir'),$this->argument('file'));
|
||||
$a = NULL;
|
||||
|
||||
$f = new File($fs->path($rel_name));
|
||||
|
||||
$m = NULL;
|
||||
if ($this->argument('ftn')) {
|
||||
$a = Address::findFTN($this->argument('ftn'));
|
||||
|
||||
} elseif (preg_match(sprintf('/^%s\.(.{3})$/',Packet::regex),$this->argument('file'),$m)) {
|
||||
$a = Address::findOrFail(hexdec($m[1]));
|
||||
}
|
||||
|
||||
foreach ($f as $packet) {
|
||||
$pkt = Packet::process($packet,$x=$f->itemName(),$f->itemSize(),$s);
|
||||
$pkt = Packet::process($packet,$x=$f->itemName(),$f->itemSize(),$a?->zone->domain);
|
||||
|
||||
$this->alert(sprintf('File Name: %s',$x));
|
||||
|
||||
$this->info(sprintf('Packet Type : %s (%s)',$pkt->type,get_class($pkt)));
|
||||
$this->info(sprintf('From : %s to %s',$pkt->fftn,$pkt->tftn));
|
||||
$this->info(sprintf('Dated : %s',$pkt->date));
|
||||
$this->info(sprintf('Password : %s (%s)',$pkt->password,$pkt->password ? 'SET' : 'NOT set'));
|
||||
$this->info(sprintf('Messages : %d',$pkt->messages->count()));
|
||||
$this->info(sprintf('Tosser : %d (%s) version %s',$pkt->software->code,$pkt->software->name,$pkt->software_ver));
|
||||
$this->info(sprintf('Capabilities: %x',$pkt->capability));
|
||||
$this->info(sprintf('From : %s to %s',$pkt->fftn->ftn,$pkt->tftn ? $pkt->tftn->ftn : $pkt->tftn_t));
|
||||
$this->info(sprintf('Dated : %s (%s) [%s]',$pkt->date,$pkt->date->timestamp,$pkt->date->tz->toOffsetName()));
|
||||
$this->info(sprintf('Password : %s (%s)',$pkt->password ?: '-',$pkt->password ? 'SET' : 'NOT set'));
|
||||
$this->info(sprintf('Messages : %d',$pkt->count()));
|
||||
$this->info(sprintf('Tosser : %d (%s) version %s (%x)',$pkt->software->code,$pkt->software->name,$pkt->software_ver,$pkt->product));
|
||||
$this->info(sprintf('Capabilities: %s',$pkt->capability));
|
||||
$this->info(sprintf('Has Errors : %s',$pkt->errors->count() ? 'YES' : 'No'));
|
||||
$this->info(sprintf('Messages : %d',$pkt->count()));
|
||||
|
||||
foreach ($pkt as $msg) {
|
||||
try {
|
||||
$this->warn(sprintf('- Date : %s',$msg->date));
|
||||
$this->warn(sprintf(' - Flags : %s',$msg->flags()->filter()->keys()->join(', ')));
|
||||
$this->warn(sprintf(' - From : %s (%s)',$msg->user_from,$msg->fftn));
|
||||
$this->warn(sprintf(' - To : %s (%s)',$msg->user_to,$msg->tftn));
|
||||
$this->warn(sprintf(' - Subject: %s',$msg->subject));
|
||||
$this->warn(sprintf(' - Area : %s',$msg->echoarea));
|
||||
echo "\n";
|
||||
|
||||
if ($msg->errors)
|
||||
foreach ($msg->errors->errors()->all() as $error)
|
||||
$this->line(' - '.$error);
|
||||
try {
|
||||
$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));
|
||||
$this->warn(sprintf(' - From : %s (%s)',$msg->from,$msg->fftn->ftn));
|
||||
if ($msg instanceof Echomail)
|
||||
$this->warn(sprintf(' - To : %s',$msg->to));
|
||||
else
|
||||
$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));
|
||||
|
||||
if ($msg->errors->count()) {
|
||||
echo "\n";
|
||||
$this->error("Errors:");
|
||||
foreach ($msg->errors->all() as $error)
|
||||
$this->error(' - '.$error);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error('! ERROR: '.$e->getMessage());
|
||||
@ -76,15 +99,17 @@ class PacketInfo extends Command
|
||||
foreach ($pkt->errors as $msg) {
|
||||
$this->error(sprintf('- Date: %s',$msg->date));
|
||||
$this->error(sprintf(' - FLAGS: %s',$msg->flags()->filter()->keys()->join(', ')));
|
||||
$this->error(sprintf(' - From: %s (%s)',$msg->user_from,$msg->fftn));
|
||||
$this->error(sprintf(' - To: %s (%s)',$msg->user_to,$msg->tftn));
|
||||
$this->error(sprintf(' - From: %s (%s)',$msg->from,$msg->fftn));
|
||||
$this->error(sprintf(' - To: %s (%s)',$msg->to,$msg->tftn));
|
||||
$this->error(sprintf(' - Subject: %s',$msg->subject));
|
||||
|
||||
foreach ($msg->errors->errors()->all() as $error)
|
||||
foreach ($msg->errors->all() as $error)
|
||||
$this->line(' - '.$error);
|
||||
}
|
||||
|
||||
$this->line("\n");
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
@ -2,14 +2,39 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
use App\Classes\File;
|
||||
use App\Classes\FTN\Packet;
|
||||
use App\Jobs\MessageProcess as Job;
|
||||
use App\Jobs\PacketProcess as Job;
|
||||
use App\Models\Address;
|
||||
|
||||
/**
|
||||
* Things to test
|
||||
* + Packet
|
||||
* - Sender doesnt exist (try and send a bounce message in the same session)
|
||||
* - Sender defined in DB by not ours
|
||||
* - Sender has wrong password
|
||||
* - Packet too old
|
||||
*
|
||||
* + Echomail
|
||||
* - Area doesnt exist (to uplink)
|
||||
* - Sender not subscribed (to uplink)
|
||||
* - Sender cannot post (to uplink)
|
||||
* - Sender has wrong address for echorea domain (to uplink)
|
||||
* - Test message in echoarea
|
||||
* - Echomail from address doesnt match packet envelope (to uplink)
|
||||
* - Echomail too old (to uplink)
|
||||
* - Rescanned dont generate notifications
|
||||
* - Rescanned dont trigger bots
|
||||
* - Some Notifications to an uplink should go to the admin instead?
|
||||
*
|
||||
* + Netmail
|
||||
* - To hub, and user not defined (reject)
|
||||
* - To hub, but user redirect (redirected)
|
||||
* - To areafix (processed)
|
||||
* - To ping (respond)
|
||||
* - With trace turned on (respond)
|
||||
*/
|
||||
class PacketProcess extends Command
|
||||
{
|
||||
/**
|
||||
@ -20,7 +45,8 @@ class PacketProcess extends Command
|
||||
protected $signature = 'packet:process'
|
||||
.' {file : Packet to process}'
|
||||
.' {--N|nobot : Dont process bots}'
|
||||
.' {ftn : System the packet is from}';
|
||||
.' {ftn? : System the packet is from}'
|
||||
.' {--Q|dontqueue : Dont queue the message}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@ -32,24 +58,28 @@ class PacketProcess extends Command
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
* @throws \App\Classes\FTN\InvalidPacketException
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(): int
|
||||
{
|
||||
$f = new File($this->argument('file'));
|
||||
$a = Address::findFTN($this->argument('ftn'));
|
||||
$rel_name = sprintf('%s/%s',config('fido.dir'),$this->argument('file'));
|
||||
|
||||
foreach ($f as $packet) {
|
||||
foreach (Packet::process($packet,$f->itemName(),$f->itemSize(),$a->system) as $msg) {
|
||||
// @todo Quick check that the packet should be processed by us.
|
||||
// @todo validate that the packet's zone is in the domain.
|
||||
$m = [];
|
||||
if ($this->argument('ftn')) {
|
||||
$ao = Address::findFTN($this->argument('ftn'));
|
||||
|
||||
$this->info(sprintf('Processing message from [%s] with msgid [%s] in (%s)',$msg->fboss,$msg->msgid,$f->pktName()));
|
||||
} elseif (preg_match(sprintf('/^%s\.(.{3})$/',Packet::regex),$this->argument('file'),$m)) {
|
||||
$ao = Address::findOrFail(hexdec($m[1]));
|
||||
|
||||
// Dispatch job.
|
||||
Job::dispatchSync($msg,$f->pktName(),$a,$a,Carbon::now(),$this->option('nobot'));
|
||||
}
|
||||
} else {
|
||||
$this->error('Unable to determine sender FTN address');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
Job::dispatchSync($rel_name,$ao->zone->domain,$this->option('dontqueue'));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
use App\Models\System;
|
||||
use App\Models\Address;
|
||||
|
||||
class PacketSystem extends Command
|
||||
{
|
||||
@ -14,7 +14,7 @@ class PacketSystem extends Command
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'packet:system'
|
||||
.' {sid : System ID}';
|
||||
.' {ftn : System address}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@ -26,21 +26,31 @@ class PacketSystem extends Command
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
* @throws \App\Classes\FTN\InvalidPacketException
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(): int
|
||||
{
|
||||
$so = System::findOrFail($this->argument('sid'));
|
||||
$ao = Address::findFTN($this->argument('ftn'));
|
||||
|
||||
foreach ($so->addresses as $ao) {
|
||||
$pkt = $ao->getEchomail(FALSE);
|
||||
$this->info(sprintf('System address [%s] has [%d] messages.',$ao->ftn,$pkt?->count()));
|
||||
foreach ($ao->system->addresses->where('validated',TRUE) as $o) {
|
||||
$pkt = $o->getEchomail();
|
||||
$this->info(sprintf('System address [%s] has [%d] echomail messages.',$o->ftn,$pkt?->count()));
|
||||
|
||||
if ($pkt) {
|
||||
foreach ($pkt as $msg)
|
||||
$this->warn(sprintf('- %s',$msg->msgid));
|
||||
$this->warn(sprintf('- %s (%s)',$msg->msgid,$msg->id));
|
||||
}
|
||||
|
||||
$pkt = $o->getNetmail();
|
||||
$this->info(sprintf('System address [%s] has [%d] netmail messages.',$o->ftn,$pkt?->count()));
|
||||
|
||||
if ($pkt) {
|
||||
foreach ($pkt as $msg)
|
||||
$this->warn(sprintf('- %s (%s)',$msg->msgid,$msg->id));
|
||||
}
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
@ -30,10 +30,10 @@ class ServerStart extends Command
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return void
|
||||
* @return int
|
||||
* @throws SocketException
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(): int
|
||||
{
|
||||
Log::info(sprintf('%s:+ Server Starting (%d)',self::LOGKEY,getmypid()));
|
||||
$o = Setup::findOrFail(config('app.id'));
|
||||
@ -71,7 +71,7 @@ class ServerStart extends Command
|
||||
if (! $start->count()) {
|
||||
Log::alert(sprintf('%s:! No servers configured to start',self::LOGKEY));
|
||||
|
||||
return;
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
pcntl_signal(SIGCHLD,SIG_IGN);
|
||||
@ -105,7 +105,7 @@ class ServerStart extends Command
|
||||
Log::info(sprintf('%s:= Finished: [%s]',self::LOGKEY,$item));
|
||||
|
||||
// Child finished we need to get out of this loop.
|
||||
exit;
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
Log::debug(sprintf('%s:- Forked for [%s] (%d)',self::LOGKEY,$item,$pid));
|
||||
@ -125,5 +125,7 @@ class ServerStart extends Command
|
||||
|
||||
// Done
|
||||
Log::debug(sprintf('%s:= Finished.',self::LOGKEY));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
35
app/Console/Commands/SystemHeartbeat.php
Normal file
35
app/Console/Commands/SystemHeartbeat.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
use App\Jobs\SystemHeartbeat as Job;
|
||||
|
||||
class SystemHeartbeat extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'system:heartbeat'
|
||||
.' {--F|force : Force poll systems that are auto hold}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Poll systems that we havent seen for a while';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
Log::info('CSH:- Triggering heartbeat to systems');
|
||||
return Job::dispatchSync($this->option('force'));
|
||||
}
|
||||
}
|
@ -29,11 +29,9 @@ class TicProcess extends Command
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(): int
|
||||
{
|
||||
// Dispatch job.
|
||||
Job::dispatchSync($this->argument('file'),$this->argument('domain'));
|
||||
|
||||
return Command::SUCCESS;
|
||||
return Job::dispatchSync($this->argument('file'),$this->argument('domain'));
|
||||
}
|
||||
}
|
@ -19,14 +19,16 @@ class UserCodeSend extends Command
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(): int
|
||||
{
|
||||
$ao = Address::findFTN($this->argument('ftn'));
|
||||
$uo = User::where('email',$this->argument('email'))->singleOrFail();
|
||||
|
||||
Notification::route('netmail',$ao->parent())->notify(new AddressLink($uo));
|
||||
Notification::route('netmail',$ao->uplink())->notify(new AddressLink($uo));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
@ -27,12 +27,14 @@ class UserMakeAdmin extends Command
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(): int
|
||||
{
|
||||
$o = User::where('email',$this->argument('email'))->firstOrfail();
|
||||
$o->admin = ! $o->admin;
|
||||
$o->save();
|
||||
|
||||
$this->info(sprintf('User [%s] %s an admin',$o->email,$o->admin ? 'IS' : 'is NOT'));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
@ -2,42 +2,45 @@
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use App\Jobs\MailSend;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
|
||||
use App\Jobs\{AddressIdleDomain,MailSend,SystemHeartbeat};
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
/**
|
||||
* The Artisan commands provided by your application.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $commands = [
|
||||
//
|
||||
];
|
||||
/**
|
||||
* The Artisan commands provided by your application.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $commands = [
|
||||
//
|
||||
];
|
||||
|
||||
/**
|
||||
* Define the application's command schedule.
|
||||
*
|
||||
* @param \Illuminate\Console\Scheduling\Schedule $schedule
|
||||
* @return void
|
||||
*/
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
$schedule->job(new MailSend(TRUE))->everyMinute()->withoutOverlapping();
|
||||
$schedule->job(new MailSend(FALSE))->twiceDaily(1,13);
|
||||
}
|
||||
/**
|
||||
* Define the application's command schedule.
|
||||
*
|
||||
* @param \Illuminate\Console\Scheduling\Schedule $schedule
|
||||
* @return void
|
||||
*/
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
$schedule->job(new MailSend(TRUE))->everyMinute()->withoutOverlapping();
|
||||
$schedule->job(new MailSend(FALSE))->twiceDaily(1,13);
|
||||
$schedule->job(new SystemHeartbeat)->hourly();
|
||||
$schedule->job(new AddressIdleDomain)->weeklyOn(0,'01:00');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the commands for the application.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function commands()
|
||||
{
|
||||
$this->load(__DIR__.'/Commands');
|
||||
/**
|
||||
* Register the commands for the application.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function commands()
|
||||
{
|
||||
$this->load(__DIR__.'/Commands');
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
}
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
}
|
9
app/Exceptions/InvalidCRCException.php
Normal file
9
app/Exceptions/InvalidCRCException.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class InvalidCRCException extends Exception
|
||||
{
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user