From b179b1b3e96b4b18ca6c681c4daef43344b49b04 Mon Sep 17 00:00:00 2001 From: Deon George Date: Mon, 26 Jul 2021 21:21:58 +1000 Subject: [PATCH] Mail routing parent/children, domain name validation, nodelist import changes and other fixes --- app/Classes/FTN/Packet.php | 2 +- app/Http/Controllers/DomainController.php | 12 +- app/Http/Controllers/SystemController.php | 32 ++- app/Http/Controllers/ZoneController.php | 26 ++- app/Jobs/NodelistImport.php | 10 +- app/Models/Address.php | 214 +++++++++++++++--- app/Models/System.php | 11 +- app/Models/Zone.php | 5 +- database/seeders/InitialSetupSeeder.php | 71 +++--- database/seeders/NodeHierarchy.php | 21 +- resources/views/about.blade.php | 1 + resources/views/domain/view.blade.php | 34 +-- resources/views/system/addedit.blade.php | 79 ++++++- resources/views/system/form-session.blade.php | 1 - tests/Feature/RoutingTest.php | 189 +++++++++++++--- 15 files changed, 537 insertions(+), 171 deletions(-) diff --git a/app/Classes/FTN/Packet.php b/app/Classes/FTN/Packet.php index 55d2cdd..c6baa0e 100644 --- a/app/Classes/FTN/Packet.php +++ b/app/Classes/FTN/Packet.php @@ -317,7 +317,7 @@ class Packet extends FTNBase private function newHeader(Address $o): void { $date = Carbon::now(); - $ao = Setup::findOrFail(config('app.id'))->system->match($o); + $ao = Setup::findOrFail(config('app.id'))->system->match($o->zone); // Create Header $this->header = [ diff --git a/app/Http/Controllers/DomainController.php b/app/Http/Controllers/DomainController.php index 2511bbe..26c8f22 100644 --- a/app/Http/Controllers/DomainController.php +++ b/app/Http/Controllers/DomainController.php @@ -14,9 +14,10 @@ class DomainController extends Controller public const NODE_RC = 1<<1; // Region public const NODE_NC = 1<<2; // Host public const NODE_HC = 1<<3; // Hub - public const NODE_PVT = 1<<4; // Pvt - public const NODE_HOLD = 1<<5; // Hold - public const NODE_DOWN = 1<<6; // Down + public const NODE_POINT = 1<<4; // Point + public const NODE_PVT = 1<<5; // Pvt + public const NODE_HOLD = 1<<6; // Hold + public const NODE_DOWN = 1<<7; // Down // http://ftsc.org/docs/frl-1002.001 public const NUMBER_MAX = 0x7fff; @@ -27,7 +28,7 @@ class DomainController extends Controller } /** - * Add or edit a node + * Add or edit a domain */ public function add_edit(Request $request,Domain $o) { @@ -35,7 +36,8 @@ class DomainController extends Controller $this->authorize('admin',$o); $request->validate([ - 'name' => 'required|max:8|unique:domains,name,'.($o->exists ? $o->id : 0), + // http://ftsc.org/docs/old/fsp-1028.002 + 'name' => 'required|max:8|regex:/^[a-z-_~]{1,8}$/|unique:domains,name,'.($o->exists ? $o->id : 0), 'dnsdomain' => 'nullable|regex:/^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){1,127}(?![0-9]*$)[a-z0-9-]+\.?)$/i|unique:domains,dnsdomain,'.($o->exists ? $o->id : NULL), 'active' => 'required|boolean', 'public' => 'required|boolean', diff --git a/app/Http/Controllers/SystemController.php b/app/Http/Controllers/SystemController.php index 4c8b0f9..43e7225 100644 --- a/app/Http/Controllers/SystemController.php +++ b/app/Http/Controllers/SystemController.php @@ -121,6 +121,32 @@ class SystemController extends Controller ] ]); + // Find the Hub address + // Find the zones /0 address, and assign it to this host. + $oo = Address::where('zone_id',$request->zone_id) + ->where('region_id',$request->region_id) + ->where('host_id',$request->host_id_new) + ->where('node_id',0) + ->where('point_id',0) + ->single(); + + // Its not defined, so we'll create it. + if (! $oo) { + $oo = new Address; + $oo->forceFill([ + 'zone_id'=>$request->zone_id, + 'region_id'=>$request->region_id, + 'host_id'=>$request->host_id_new, + 'node_id'=>0, + 'point_id'=>0, + 'role'=>DomainController::NODE_NC, + ]); + } + + $oo->system_id = $request->system_id; + $oo->active = TRUE; + $o->addresses()->save($oo); + $oo = new Address; $oo->zone_id = $request->post('zone_id'); $oo->region_id = $request->post('region_id'); @@ -200,8 +226,8 @@ class SystemController extends Controller $validate = $request->validate([ 'zone_id' => 'required|exists:zones,id', 'sespass' => 'required|string|min:4', - 'pktpass' => 'required|string|min:4|max:8', - 'ticpass' => 'required|string|min:4', + 'pktpass' => 'nullable|string|min:4|max:8', + 'ticpass' => 'nullable|string|min:4', 'fixpass' => 'required|string|min:4', ]); @@ -242,6 +268,8 @@ class SystemController extends Controller return redirect()->action([self::class,'home']); } + $o->load(['addresses.zone.domain']); + return view('system.addedit') ->with('o',$o); } diff --git a/app/Http/Controllers/ZoneController.php b/app/Http/Controllers/ZoneController.php index 667b348..87c9744 100644 --- a/app/Http/Controllers/ZoneController.php +++ b/app/Http/Controllers/ZoneController.php @@ -5,7 +5,7 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Validation\Rule; -use App\Models\Zone; +use App\Models\{Address,Zone}; class ZoneController extends Controller { @@ -64,6 +64,30 @@ class ZoneController extends Controller $o->save(); + // Find the zones 0/0 address, and assign it to this host. + $ao = Address::where('zone_id',$request->zone_id) + ->where('region_id',0) + ->where('host_id',0) + ->where('node_id',0) + ->where('point_id',0) + ->single(); + + // Its not defined, so we'll create it. + if (! $ao) { + $ao = new Address; + $ao->forceFill([ + 'region_id'=>0, + 'host_id'=>0, + 'node_id'=>0, + 'point_id'=>0, + 'role'=>DomainController::NODE_ZC, + ]); + } + + $ao->system_id = $request->system_id; + $ao->active = TRUE; + $o->addresses()->save($ao); + return redirect()->action([self::class,'home']); } diff --git a/app/Jobs/NodelistImport.php b/app/Jobs/NodelistImport.php index 49a6d2c..3791121 100644 --- a/app/Jobs/NodelistImport.php +++ b/app/Jobs/NodelistImport.php @@ -89,7 +89,6 @@ class NodelistImport implements ShouldQueue switch ($fields[0]) { case 'Zone': $zone = $fields[1]; - // We ignore the Zone entries, since they are dynamically created. Zone::unguard(); $zo = Zone::firstOrNew([ 'zone_id'=>$zone, @@ -99,7 +98,7 @@ class NodelistImport implements ShouldQueue Zone::reguard(); $region = 0; - $host = $fields[1]; + $host = 0; $hub_id = NULL; $role = DomainController::NODE_ZC; @@ -118,8 +117,7 @@ class NodelistImport implements ShouldQueue $hub_id = NULL; $role = DomainController::NODE_NC; - // We ignore the Host entries, since they are dynamically created. - continue 2; + break; case 'Hub': $node = $fields[1]; @@ -243,10 +241,10 @@ class NodelistImport implements ShouldQueue try { $so->addresses()->save($ao); - if ($role == DomainController::NODE_HC) + if ($ao->role == DomainController::NODE_HC) $hub_id = $ao->id; - $this->no->addresses()->attach($ao,['role'=>$role]); + $this->no->addresses()->attach($ao,['role'=>$ao->role]); } catch (\Exception $e) { Log::error(sprintf('%s:Error with line [%s] (%s)',self::LOGKEY,$line,$e->getMessage()),['fields'=>$fields]); diff --git a/app/Models/Address.php b/app/Models/Address.php index 114ea86..c5db405 100644 --- a/app/Models/Address.php +++ b/app/Models/Address.php @@ -30,37 +30,150 @@ class Address extends Model /* RELATIONS */ /** - * Find children dependant on this record + * Find children dependent on this record */ public function children() { - switch (strtolower($this->role)) { - case 'region': - return $this->hasMany(self::class,'region_id','region_id') - ->where('zone_id',$this->zone_id) - ->where(function($q) { - return $q->where('host_id',0) - ->orWhere('role',DomainController::NODE_NC); - }) - ->where('id','<>',$this->id); + // We have no session data for this address, by definition it has no children + if (! $this->session('sespass')) + return $this->hasMany(self::class,'id','void'); - case 'host': - return $this->hasMany(self::class,'host_id','host_id') + switch ($this->role) { + case DomainController::NODE_ZC: + $children = self::select('addresses.*') + ->where('zone_id',$this->zone_id); + + break; + + case DomainController::NODE_RC: + $children = self::select('addresses.*') + ->where('zone_id',$this->zone_id) + ->where('region_id',$this->region_id); + + break; + + case DomainController::NODE_NC: + $children = self::select('addresses.*') ->where('zone_id',$this->zone_id) ->where('region_id',$this->region_id) - ->whereNull('hub_id') - ->where('id','<>',$this->id); + ->where('host_id',$this->host_id); - case 'hub': - return $this->hasMany(self::class,'hub_id','id'); + break; - case 'node': + case DomainController::NODE_HC: + // Identify our children. + $children = self::select('addresses.*') + ->where('hub_id',$this->id); + + break; + + case DomainController::NODE_ACTIVE: + case DomainController::NODE_POINT: // Nodes dont have children, but must return a relationship instance return $this->hasOne(self::class,NULL,'void'); default: - throw new Exception('Unknown role: '.$this->role); + throw new Exception('Unknown role: '.serialize($this->role)); } + + // Remove any children that we have session details for (SAME AS HC) + $sessions = self::select('hubnodes.*') + ->join('system_zone',['system_zone.system_id'=>'addresses.system_id','system_zone.zone_id'=>'addresses.zone_id']) + ->join('addresses as hubnodes',['hubnodes.zone_id'=>'addresses.zone_id','hubnodes.id'=>'addresses.id']) + ->where('addresses.zone_id',$this->zone_id) + ->where('addresses.system_id','<>',$this->system_id) + ->whereIN('addresses.system_id',$children->get()->pluck('system_id')); + + // For each of the session, identify their children + $session_kids = collect(); + foreach ($sessions->get() as $so) + $session_kids = $session_kids->merge(($x=$so->children) ? $x->pluck('id') : []); + + // ZC's receive all mail, except for defined nodes, and defined hubs/hosts/rcs + return $this->hasMany(self::class,'zone_id','zone_id') + ->whereIn('id',$children->get()->pluck('id')->toArray()) + ->whereNotIn('id',$sessions->get()->pluck('id')->toArray()) + ->whereNotIn('id',$session_kids->toArray()) + ->where('system_id','<>',$this->system_id) + ->select('addresses.*') + ->orderBy('region_id') + ->orderBy('host_id') + ->orderBy('node_id') + ->orderBy('point_id') + ->with(['zone.domain']); + } + + /** + * Who we send this systems mail to. + * + * @return Address|null + * @throws Exception + */ + public function parent(): ?Address + { + // If we are have session password, then we dont have a parent + if ($this->session('sespass')) + return $this; + + switch ($this->role) { + // ZCs dont have parents. + case DomainController::NODE_ZC: + return NULL; + + // RC + case DomainController::NODE_RC: + $parent = self::where('zone_id',$this->zone_id) + ->where('region_id',0) + ->where('host_id',0) + ->where('node_id',0) + ->single(); + + break; + + // Hosts + case DomainController::NODE_NC: + // See if we have a RC + $parent = self::where('zone_id',$this->zone_id) + ->where('region_id',$this->region_id) + ->where('host_id',0) + ->where('node_id',0) + ->single(); + + if (! $parent) { + // See if we have a RC + $parent = self::where('zone_id',$this->zone_id) + ->where('region_id',0) + ->where('host_id',0) + ->where('node_id',0) + ->single(); + } + + break; + + // Hubs + case DomainController::NODE_HC: + // Normal Nodes + case DomainController::NODE_ACTIVE: + // If we are a child of a hub, then check our hub + $parent = (($this->hub_id) + ? self::where('id',$this->hub_id) + : self::where('zone_id',$this->zone_id) + ->where('region_id',$this->region_id) + ->where('host_id',$this->host_id) + ->where('node_id',0)) + ->single(); + + break; + + case DomainController::NODE_POINT: + // @todo Points - if the boss is defined, we should return it. + return NULL; + + default: + throw new Exception('Unknown role: '.serialize($this->role)); + } + + return $parent?->parent(); } public function system() @@ -95,23 +208,21 @@ class Address extends Model return sprintf('%s.%d',$this->getFTN3DAttribute(),$this->point_id); } - public function getRoleAttribute($value) + public function getRoleNameAttribute(): string { - switch ($value) { - case DomainController::NODE_ZC; - return 'Zone'; - case DomainController::NODE_RC; - return 'Region'; - case DomainController::NODE_NC; - return 'Host'; - case DomainController::NODE_HC; - return 'Hub'; - case DomainController::NODE_PVT; - return 'PVT'; - case DomainController::NODE_DOWN; - return 'DOWN'; - case NULL: - return 'Node'; + switch ($this->role) { + case DomainController::NODE_ACTIVE: + return 'NODE'; + case DomainController::NODE_ZC: + return 'ZC'; + case DomainController::NODE_RC: + return 'RC'; + case DomainController::NODE_NC: + return 'NC'; + case DomainController::NODE_HC: + return 'HUB'; + case DomainController::NODE_POINT: + return 'POINT'; default: return '?'; } @@ -130,17 +241,48 @@ class Address extends Model { $ftn = self::parseFTN($ftn); + // Are we looking for a region address + if (($ftn['f'] === 0) && $ftn['p'] === 0) { + $o = (new self)->active() + ->select('addresses.*') + ->where('zones.zone_id',$ftn['z']) + ->where(function($q) use ($ftn) { + return $q + ->where(function($q) use ($ftn) { + return $q->where('region_id',$ftn['n']) + ->where('host_id',0); + }); + }) + ->where('node_id',$ftn['f']) + ->where('point_id',$ftn['p']) + ->join('zones',['zones.id'=>'addresses.zone_id']) + ->join('domains',['domains.id'=>'zones.domain_id']) + ->where('zones.active',TRUE) + ->where('domains.active',TRUE) + ->where('addresses.active',TRUE) + ->when($ftn['d'],function($query,$domain) { + $query->where('domains.name',$domain); + }) + ->when((! $ftn['d']),function($query) { + $query->where('domains.default',TRUE); + }) + ->single(); + + if ($o && $o->system->active) + return $o; + } + $o = (new self)->active() ->select('addresses.*') ->where('zones.zone_id',$ftn['z']) ->where('host_id',$ftn['n']) + ->where('node_id',$ftn['f']) + ->where('point_id',$ftn['p']) ->join('zones',['zones.id'=>'addresses.zone_id']) ->join('domains',['domains.id'=>'zones.domain_id']) ->where('zones.active',TRUE) ->where('domains.active',TRUE) ->where('addresses.active',TRUE) - ->where('node_id',$ftn['f']) - ->where('point_id',$ftn['p']) ->when($ftn['d'],function($query,$domain) { $query->where('domains.name',$domain); }) diff --git a/app/Models/System.php b/app/Models/System.php index 34353c4..ce5afdd 100644 --- a/app/Models/System.php +++ b/app/Models/System.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Model; use App\Http\Controllers\DomainController; use App\Traits\ScopeActive; +use Illuminate\Support\Collection; class System extends Model { @@ -61,12 +62,12 @@ class System extends Model /** * Return the system's address in the same zone * - * @param Address $o - * @return Address + * @param Zone $o + * @return Collection */ - public function match(Address $o): Address + public function match(Zone $o): Collection { - return $this->addresses->where('zone_id',$o->zone_id)->first(); + return $this->addresses->where('zone_id',$o->id); } /** @@ -85,6 +86,8 @@ class System extends Model return sprintf('RC-%s-%05d',$o->zone->domain->name,$o->region_id); case DomainController::NODE_NC; + return sprintf('NC-%s-%05d',$o->zone->domain->name,$o->host_id); + case DomainController::NODE_HC; case NULL: default: diff --git a/app/Models/Zone.php b/app/Models/Zone.php index 736189f..cc73a9f 100644 --- a/app/Models/Zone.php +++ b/app/Models/Zone.php @@ -25,7 +25,10 @@ class Zone extends Model public function addresses() { - return $this->hasMany(Address::class); + return $this->hasMany(Address::class) + ->active() + ->FTNorder() + ->with(['system.sessions','system.setup','zone.domain']); } public function domain() diff --git a/database/seeders/InitialSetupSeeder.php b/database/seeders/InitialSetupSeeder.php index d745a1c..77965c6 100644 --- a/database/seeders/InitialSetupSeeder.php +++ b/database/seeders/InitialSetupSeeder.php @@ -2,10 +2,11 @@ namespace Database\Seeders; +use Carbon\Carbon; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; -use App\Models\Software; +use App\Models\{Domain,Software,System,Zone}; class InitialSetupSeeder extends Seeder { @@ -32,65 +33,49 @@ class InitialSetupSeeder extends Seeder 'type'=>Software::SOFTWARE_MAILER, ]); - DB::table('domains')->insert([ + $so = new System; + $so->forceFill([ + 'name'=>'Clearing Houz - Dev', + 'sysop'=>'System Sysop', + 'location'=>'Melbourne, AU', + 'active'=>TRUE, + ]); + $so->save(); + + $do = new Domain; + $do->forceFill([ 'name'=>'private', 'default'=>TRUE, 'active'=>TRUE, 'public'=>TRUE, 'notes'=>'PrivateNet: Internal Testing Network' ]); + $do->save(); - DB::table('zones')->insert([ + $zo = new Zone; + $zo->forceFill([ 'zone_id'=>'10', - 'domain_id'=>1, 'active'=>TRUE, + 'system_id'=>$so->id, ]); - - DB::table('nodes')->insert([ - 'zone_id'=>'1', - 'host_id'=>'999', - 'node_id'=>'2', - 'is_host'=>TRUE, - 'active'=>TRUE, - 'system'=>'FTN Clearing House Dev', - 'sysop'=>'Deon George', - 'location'=>'Parkdale, AUS', - 'email'=>'deon@leenooks.net', - 'address'=>'10.1.3.165', - 'port'=>24554, - 'protocol_id'=>1, - 'software_id'=>1, - ]); - - DB::table('nodes')->insert([ - 'zone_id'=>'1', - 'host_id'=>'999', - 'node_id'=>'1', - 'is_host'=>TRUE, - 'active'=>TRUE, - 'system'=>'Alterant MailHUB DEV', - 'sysop'=>'Deon George', - 'location'=>'Parkdale, AUS', - 'email'=>'deon@leenooks.net', - 'address'=>'d-1-4.ipv4.leenooks.vpn', - 'port'=>14554, - 'sespass'=>'PASSWORD', - 'protocol_id'=>1, - 'software_id'=>1, - ]); + $do->zones()->save($zo); DB::table('setups')->insert([ - 'opt_md'=>'1', - ]); - - DB::table('node_setup')->insert([ - 'node_id'=>'1', - 'setup_id'=>'1', + 'system_id'=>$so->id, + 'zmodem'=>0, + 'emsi_protocols'=>0, + 'binkp'=>0, + 'protocols'=>0, + 'permissions'=>0, + 'options'=>0, ]); DB::table('users')->insert([ 'name'=>'Deon George', 'email'=>'deon@leenooks.net', + 'email_verified_at'=>Carbon::now(), + 'admin'=>TRUE, + 'active'=>TRUE, 'password'=>'$2y$10$bJQDLfxnKrh6o5Sa02MZOukXcLTNQiByXSTJ7fTr.kHMpV2wxbG6.', ]); } diff --git a/database/seeders/NodeHierarchy.php b/database/seeders/NodeHierarchy.php index 6c8a74d..9b74bd9 100644 --- a/database/seeders/NodeHierarchy.php +++ b/database/seeders/NodeHierarchy.php @@ -20,7 +20,7 @@ class NodeHierarchy extends Seeder { DB::table('domains') ->insert([ - 'name'=>'Domain A', + 'name'=>'domain-a', 'active'=>TRUE, 'public'=>TRUE, 'default'=>FALSE, @@ -30,7 +30,7 @@ class NodeHierarchy extends Seeder DB::table('domains') ->insert([ - 'name'=>'Domain B', + 'name'=>'domain-b', 'active'=>TRUE, 'public'=>TRUE, 'default'=>FALSE, @@ -38,7 +38,7 @@ class NodeHierarchy extends Seeder 'updated_at'=>Carbon::now(), ]); - foreach (['Domain A','Domain B'] as $domain) { + foreach (['domain-a','domain-b'] as $domain) { $domain = Domain::where('name',$domain)->singleOrFail(); $this->hierarchy($domain,100); $this->hierarchy($domain,101); @@ -66,6 +66,19 @@ class NodeHierarchy extends Seeder ]); $zo = Zone::where('zone_id',$zoneid)->where('domain_id',$domain->id)->singleOrFail(); + DB::table('addresses') + ->insert([ + 'zone_id'=>$zo->id, + 'active'=>TRUE, + 'region_id'=>0, + 'host_id'=>0, + 'node_id'=>0, + 'point_id'=>0, + 'system_id'=>$so->id, + 'role'=>DomainController::NODE_ZC, + 'created_at'=>Carbon::now(), + 'updated_at'=>Carbon::now(), + ]); // Nodes foreach ($nodes as $nid) { @@ -120,7 +133,7 @@ class NodeHierarchy extends Seeder // Hosts foreach ($hosts as $hid) { - $hostid = $rid*100+$hid; + $hostid = $rid*10+$hid; $so = $this->system(sprintf('Host %03d:%03d/0.0@%s (Region %03d)',$zoneid,$hostid,$domain->name,$rid)); DB::table('addresses') ->insert([ diff --git a/resources/views/about.blade.php b/resources/views/about.blade.php index a9c2d3a..3eb7768 100644 --- a/resources/views/about.blade.php +++ b/resources/views/about.blade.php @@ -39,6 +39,7 @@ If you have more than 1 BBS, then the Clearing House can receive all your mail f