setup = $setup; // Some initialisation details switch (get_class($this)) { case Binkp::class: $this->mailer_id = Mailer::where('name','BINKP')->sole()->id; break; case DNS::class: case Zmodem::class: break; case EMSI::class: $this->mailer_id = Mailer::where('name','EMSI')->sole()->id; break; default: throw new \Exception('not handled'.get_class($this)); } } /** * @throws \Exception */ public function __get($key) { switch ($key) { case 'ls_SkipGuard': /* double-skip protection on/off */ case 'rxOptions': /* Options from ZRINIT header */ return $this->comms[$key] ?? 0; case 'ls_rxAttnStr': return $this->comms[$key] ?? ''; default: throw new \Exception('Unknown key: '.$key); } } /** * @throws \Exception */ public function __set($key,$value) { switch ($key) { case 'ls_rxAttnStr': case 'ls_SkipGuard': case 'rxOptions': $this->comms[$key] = $value; break; case 'client': $this->{$key} = $value; break; default: throw new \Exception('Unknown key: '.$key); } } /* Capabilities are what we negotitate with the remote and are valid for the session */ /** * Clear a capability bit * * @param int $cap (F_*) * @param int $val (O_*) * @return void */ public function capClear(int $cap,int $val): void { if (! array_key_exists($cap,$this->capability)) $this->capability[$cap] = 0; $this->capability[$cap] &= ~$val; } /** * Get a session bit (SE_*) * * @param int $cap (F_*) * @param int $val (O_*) * @return bool */ protected function capGet(int $cap,int $val): bool { if (! array_key_exists($cap,$this->capability)) $this->capability[$cap] = 0; if ($val === self::O_WE) return $this->capGet($cap,self::O_WANT) && $this->capGet($cap,self::O_THEY); return $this->capability[$cap] & $val; } /** * Set a session bit (SE_*) * * @param int $cap (F_*) * @param int $val (O_*) */ protected function capSet(int $cap,int $val): void { if (! array_key_exists($cap,$this->capability) || $val === 0) $this->capability[$cap] = 0; $this->capability[$cap] |= $val; } /** * We got an error, close anything we are have open * * @throws \Exception */ protected function error_close(): void { if ($this->send->fd) $this->send->close(FALSE,$this->node); if ($this->recv->fd) $this->recv->close(); } /** * Incoming Protocol session * * @param SocketClient $client * @return int * @throws SocketException */ public function onConnect(SocketClient $client): int { $pid = pcntl_fork(); if ($pid === -1) throw new SocketException(SocketException::CANT_ACCEPT,'Could not fork process'); // If our parent returns a PID, we've forked if ($pid) Log::info(sprintf('%s:+ New connection from [%s], thread [%d] created',self::LOGKEY,$client->address_remote,$pid)); // This is the new thread else { Log::withContext(['pid'=>getmypid()]); $this->session($client,(new Address)); } return $pid; } /* O_* determine what features processing is availabile */ /** * Clear an option bit (O_*) * * @param int $key * @return void */ protected function optionClear(int $key): void { $this->options &= ~$key; } /** * Get an option bit (O_*) * * @param int $key * @return int */ protected function optionGet(int $key): int { return ($this->options & $key); } /** * Set an option bit (O_*) * * @param int $key * @return void */ protected function optionSet(int $key): void { $this->options |= $key; } /** * Our addresses to send to the remote * * @return Collection * @throws \Exception */ protected function our_addresses(): Collection { if ($this->setup->optionGet(Setup::O_HIDEAKA,'options_options')) { $addresses = collect(); foreach (($this->originate ? $this->node->aka_remote_authed : $this->node->aka_remote) as $ao) $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 = our_address(); Log::debug(sprintf('%s:- Presenting ALL our AKAs [%s]',self::LOGKEY,$addresses->pluck('ftn')->join(','))); } return $addresses; } /** * Setup a session with a remote client * * @param SocketClient $client Socket details of session * @param Address|null $o If we have an address, we originated a session to this Address * @return int * @throws \Exception */ public function session(SocketClient $client,Address $o=NULL): int { if ($o->exists) Log::withContext(['ftn'=>$o->ftn]); // This sessions options $this->options = 0; $this->session = 0; $this->capability = []; // Our files that we are sending/receive $this->send = new Send; $this->recv = new Receive; if ($o) { // The node we are communicating with $this->node = new Node; $this->originate = $o->exists; // If we are connecting to a node if ($o->exists) { Log::debug(sprintf('%s:+ Originating a connection to [%s]',self::LOGKEY,$o->ftn)); $this->node->originate($o); } else { $this->optionSet(self::O_INB); } } // We are an IP node $this->optionSet(self::O_TCP); $this->client = $client; $this->down = app()->isDownForMaintenance(); switch (get_class($this)) { case EMSI::class: Log::debug(sprintf('%s:- Starting EMSI',self::LOGKEY)); $rc = $this->protocol_init(); if ($rc < 0) { Log::error(sprintf('%s:! Unable to start EMSI [%d]',self::LOGKEY,$rc)); return self::S_FAILURE; } $rc = $this->protocol_session($this->originate); break; case Binkp::class: Log::debug(sprintf('%s:- Starting BINKP',self::LOGKEY)); $rc = $this->protocol_session($this->originate); break; default: Log::error(sprintf('%s:! Unsupported session type [%s]',self::LOGKEY,get_class($this))); return self::S_FAILURE; } // @todo These flags determine when we connect to the remote. // If the remote indicated that they dont support file requests (NRQ) or temporarily hold them (HRQ) if (($this->node->optionGet(self::O_NRQ) && (! $this->setup->optionGet(EMSI::F_IGNORE_NRQ,'emsi_options'))) || $this->node->optionGet(self::O_HRQ)) $rc |= self::S_HOLDR; if ($this->optionGet(self::O_HXT)) $rc |= self::S_HOLDX; if ($this->optionGet(self::O_HAT)) $rc |= self::S_HOLDA; Log::info(sprintf('%s:= Total: %s - %d:%02d:%02d online, (%d) %lu%s sent, (%d) %lu%s received - %s', self::LOGKEY, $this->node->address ? $this->node->address->ftn : 'Unknown', $this->node->session_time/3600, $this->node->session_time%3600/60, $this->node->session_time%60, $this->send->total_sent,$this->send->total_sent_bytes,'b', $this->recv->total_recv,$this->recv->total_recv_bytes,'b', (($rc & self::S_MASK) === self::S_OK) ? 'Successful' : 'Failed', )); // Add unknown FTNs to the DB $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) // @todo For disabled zones, we shouldnt refuse to record the address // @todo If the system hasnt presented an address for a configured period (eg: 30 days) assume it no longer carries it if ((! Address::findFTN($aka)) && ($oo=Address::createFTN($aka,$so))) { $oo->validated = TRUE; $oo->save(); } // Log session in DB $slo = new SystemLog; $slo->items_sent = $this->send->total_sent; $slo->items_sent_size = $this->send->total_sent_bytes; $slo->items_recv = $this->recv->total_recv; $slo->items_recv_size = $this->recv->total_recv_bytes; $slo->mailer_id = $this->mailer_id; $slo->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(); } } return $rc; } /* SE_* flags determine our session processing status, at any point in time */ /** * Clear a session bit (SE_*) * * @param int $key */ protected function sessionClear(int $key): void { $this->session &= ~$key; } /** * Get a session bit (SE_*) * * @param int $key * @return bool */ protected function sessionGet(int $key): bool { return ($this->session & $key); } /** * Set a session bit (SE_*) * * @param int $key */ protected function sessionSet(int $key): void { $this->session |= $key; } }