diff --git a/app/Classes/FTNMessage.php b/app/Classes/FTNMessage.php index 82864c9..8d205ce 100644 --- a/app/Classes/FTNMessage.php +++ b/app/Classes/FTNMessage.php @@ -5,6 +5,7 @@ namespace App\Classes; use Illuminate\Support\Arr; use App\Exceptions\InvalidFidoPacketException; +use App\Traits\GetNode; /** * Class FTNMessage @@ -14,6 +15,8 @@ use App\Exceptions\InvalidFidoPacketException; */ class FTNMessage extends FTN { + use GetNode; + private $src = NULL; // SRC N/F from packet private $dst = NULL; // DST N/F from packet @@ -106,6 +109,7 @@ class FTNMessage extends FTN case 'fp': return ftn_address_split($this->_fqfa,'p'); case 'fqfa': return $this->_fqfa; + case 'fqda': return $this->_fqda; // Echomails dont have a fully qualified from address case 'tz': return ftn_address_split($this->_fqda,'z'); @@ -136,7 +140,7 @@ class FTNMessage extends FTN { case 'fqfa': case 'fqda': - $this->{'_'.$k} = $v; + $this->{'_'.$k} = $this->get_node(ftn_address_split($v),TRUE); if ($this->_fqfa AND $this->_fqda) $this->intl = sprintf('%s %s',$this->_fqda,$this->_fqfa); @@ -178,6 +182,9 @@ class FTNMessage extends FTN $return .= $this->from."\00"; $return .= $this->subject."\00"; + if ($this->type == 'echomail') + $return .= "AREA:".$this->echoarea."\r"; + // Add some kludges $return .= "\01MSGID ".$this->_fqfa." 1"."\r"; diff --git a/app/Classes/FTNPacket.php b/app/Classes/FTNPacket.php index 54ca05c..cc90d82 100644 --- a/app/Classes/FTNPacket.php +++ b/app/Classes/FTNPacket.php @@ -2,11 +2,15 @@ namespace App\Classes; -use App\Exceptions\InvalidFidoPacketException; use Carbon\Carbon; +use App\Exceptions\InvalidFidoPacketException; +use App\Traits\GetNode; + class FTNPacket extends FTN { + use GetNode; + public $pktsrc = NULL; public $pktdst = NULL; private $pktver = NULL; @@ -92,12 +96,18 @@ class FTNPacket extends FTN } } + // @note - messages in this object have the same next destination public function __toString(): string { // @todo - is this appropriate to set here $this->date = now(); - $this->pktsrc = '10:1/5.0'; - $this->pktdst = '10:1/0.0'; + $this->pktsrc = (string)$this->get_node(ftn_address_split('10:1/5.0'),TRUE); + + // @todo + if ($this->messages->first()->type == 'echomail') + $this->pktdst = (string)$this->messages->first()->fqfa->uplink; + else + $this->pktdst = (string)$this->messages->first()->fqda->uplink; $this->software['prodcode-lo'] = 0x00; $this->software['prodcode-hi'] = 0xde; @@ -165,6 +175,7 @@ class FTNPacket extends FTN public function addMessage(FTNMessage $o) { + // @todo Check that this message is for the same uplink. $this->messages->push($o); } diff --git a/app/Classes/File/Item.php b/app/Classes/File/Item.php new file mode 100644 index 0000000..8f16469 --- /dev/null +++ b/app/Classes/File/Item.php @@ -0,0 +1,135 @@ +action |= $action; + + switch ($action) { + case self::I_SEND: + if (! is_string($file)) + throw new Exception('Invalid object creation - file should be a string'); + + if (! file_exists($file)) + throw new FileNotFoundException('Item doesnt exist: '.$file); + + if (! is_readable($file)) + throw new UnreadableFileException('Item cannot be read: '.$file); + + $this->file_name = $file; + $x = stat($file); + $this->file_size = $x['size']; + $this->file_mtime = $x['mtime']; + + break; + + case self::I_RECV; + $keys = ['name','mtime','size']; + + if (! is_array($file) || array_diff(array_keys($file),$keys)) + throw new Exception('Invalid object creation - file is not a valid array :'.serialize(array_diff(array_keys($file),$keys))); + + $this->file_name = $file['name']; + $this->file_size = $file['size']; + $this->file_mtime = $file['mtime']; + + break; + + default: + throw new Exception('Unknown action: '.$action); + } + + $this->file_type |= $this->whatType(); + } + + /** + * @throws Exception + */ + public function __get($key) + { + switch($key) { + case 'mtime': + case 'name': + case 'size': + if ($this->action & self::I_RECV) + return $this->{'file_'.$key}; + + throw new Exception('Invalid request for key: '.$key); + + case 'recvas': + return '/tmp/'.$this->file_name; // @todo this should be inbound temp + + case 'sendas': + return basename($this->file_name); + + default: + throw new Exception('Unknown key: '.$key); + } + } + + protected function isType(int $type): bool + { + return $this->file_type & $type; + } + + private function whatType(): int + { + static $ext = ['su','mo','tu','we','th','fr','sa','req']; + + $x = strrchr($this->file_name,'.'); + if (! $x || (strlen(substr($x,1)) != 3)) + return self::IS_FILE; + + if (strcasecmp(substr($x,2),'lo') == 0) + return self::IS_FLO; + + if (strcasecmp(substr($x,1),'pkt') == 0) + return self::IS_PKT; + + if (strcasecmp(substr($x,1),'req') == 0) + return self::IS_REQ; + + for ($i=0;$ilist = collect(); + $this->receiving = NULL; + $this->file_pos = 0; + $this->f = NULL; + } + + public function __get($key) + { + switch ($key) { + case 'fd': + return is_resource($this->f); + + case 'filepos': + return $this->file_pos; + + case 'mtime': + case 'name': + case 'size': + return $this->receiving ? $this->receiving->{'file_'.$key} : NULL; + + case 'to_get': + return $this->list + ->filter(function($item) { return ($item->action & self::I_RECV) && $item->received === FALSE; }) + ->count(); + + case 'total_recv': + return $this->list + ->filter(function($item) { return ($item->action & self::I_RECV) && $item->received === TRUE; }) + ->count(); + + case 'total_recv_bytes': + return $this->list + ->filter(function($item) { return ($item->action & self::I_RECV) && $item->received === TRUE; }) + ->sum(function($item) { return $item->file_size; }); + + default: + throw new Exception('Unknown key: '.$key); + } + } + + /** + * Close the file descriptor for our incoming file + * + * @throws Exception + */ + public function close(): void + { + if (! $this->f) + throw new Exception('No file to close'); + + if (! $this->file_pos != $this->receiving->file_size) + Log::warning(sprintf('%s: - Closing [%s], but missing [%d] bytes',__METHOD__,$this->receiving->file_name,$this->receiving->file_size-$this->file_pos)); + + $this->receiving->received = TRUE; + + $end = time()-$this->start; + Log::debug(sprintf('%s: - Closing [%s], received in [%d]',__METHOD__,$this->receiving->file_name,$end)); + + fclose($this->f); + $this->file_pos = 0; + $this->receiving = NULL; + $this->f = NULL; + } + + /** + * Open the file descriptor to receive a file + * + * @param bool $check + * @return bool + * @throws Exception + */ + public function open(bool $check=FALSE): bool + { + Log::debug(sprintf('%s: + Start [%d]',__METHOD__,$check)); + + // Check we can open this file + // @todo + // @todo implement return 2 - SKIP file + // @todo implement return 4 - SUSPEND(?) file + if ($check) { + return 0; + } + + if (! $this->receiving) + throw new Exception('No files currently receiving'); + + $this->file_pos = 0; + $this->start = time(); + + Log::debug(sprintf('%s: - Opening [%s]',__METHOD__,$this->receiving->recvas)); + $this->f = fopen($this->receiving->recvas,'wb'); + if (! $this->f) { + Log::error(sprintf('%s: ! Unable to open file [%s] for writing',__METHOD__,$this->receiving->file_name)); + return 3; // @todo change to const + } + + Log::info(sprintf('%s: = End - File [%s] opened for writing',__METHOD__,$this->receiving->file_name)); + return 0; // @todo change to const + } + + /** + * Add a new file to receive + * + * @param array $file + * @throws Exception + */ + public function new(array $file): void + { + Log::debug(sprintf('%s: + Start',__METHOD__),['file'=>$file]); + + if ($this->receiving) + throw new Exception('Can only have 1 file receiving at a time'); + + $o = new Item($file,self::I_RECV); + $this->list->push($o); + + $this->receiving = $o; + } + + /** + * Write data to the file we are receiving + * + * @param string $buf + * @return int + * @throws Exception + */ + public function write(string $buf): int + { + if (! $this->f) + throw new Exception('No file open for read'); + + if ($this->file_pos+strlen($buf) > $this->receiving->file_size) + throw new Exception(sprintf('Too many bytes received [%d] (%d)?',$this->file_pos+strlen($buf),$this->receiving->file_size)); + + $rc = fwrite($this->f,$buf); + + if ($rc === FALSE) + throw new FileException('Error while writing to file'); + + $this->file_pos += $rc; + Log::debug(sprintf('%s: - Write [%d] bytes, file pos now [%d] of [%d]',__METHOD__,$rc,$this->file_pos,$this->receiving->file_size)); + + return $rc; + } +} \ No newline at end of file diff --git a/app/Classes/File/Send.php b/app/Classes/File/Send.php new file mode 100644 index 0000000..317d601 --- /dev/null +++ b/app/Classes/File/Send.php @@ -0,0 +1,235 @@ +list = collect(); + $this->sending = NULL; + $this->file_pos = 0; + $this->f = NULL; + } + + public function __get($key) + { + switch ($key) { + case 'fd': + return is_resource($this->f); + + case 'file_count': + return $this->list + ->filter(function($item) { return $item->isType(self::IS_FILE); }) + ->count(); + + case 'file_size': + return $this->list + ->filter(function($item) { return $item->isType(self::IS_FILE); }) + ->sum(function($item) { return $item->file_size; }); + + case 'filepos': + return $this->file_pos; + + case 'mail_count': + return $this->list + ->filter(function($item) { return $item->isType(self::IS_ARC|self::IS_PKT); }) + ->count(); + + case 'mail_size': + return $this->list + ->filter(function($item) { return $item->isType(self::IS_ARC|self::IS_PKT); }) + ->sum(function($item) { return $item->file_size; }); + + case 'sendas': + return $this->sending ? $this->sending->{$key} : NULL; + + case 'name': + case 'mtime': + case 'size': + return $this->sending ? $this->sending->{'file_'.$key} : NULL; + + case 'total_sent': + return $this->list + ->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === TRUE; }) + ->count(); + + case 'total_sent_bytes': + return $this->list + ->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === TRUE; }) + ->sum(function($item) { return $item->file_size; }); + + case 'total_count': + return $this->list + ->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === FALSE; }) + ->count(); + + case 'total_size': + return $this->list + ->sum(function($item) { return $item->file_size; }); + + default: + throw new Exception('Unknown key: '.$key); + } + } + + /** + * Add a file to the list of files to send + * + * @param string $file + * @throws Exception + */ + public function add(string $file): void + { + Log::debug(sprintf('%s: + Start [%s]',__METHOD__,$file)); + + try { + $this->list->push(new Item($file,self::I_SEND)); + + } catch (FileNotFoundException) { + Log::error(sprintf('%s: ! Item [%s] doesnt exist',__METHOD__,$file)); + return; + + } catch (UnreadableFileException) { + Log::error(sprintf('%s: ! Item [%s] cannot be read',__METHOD__,$file)); + return; + + // Uncaught, rethrow the error + } catch (Exception $e) { + throw new Exception($e->getMessage()); + } + } + + /** + * Close the file descriptor of the file we are sending + * + * @param bool $successful + * @throws Exception + */ + public function close(bool $successful): void + { + if (! $this->f) + throw new Exception('No file to close'); + + if ($successful) { + $this->sending->sent = TRUE; + $end = time()-$this->start; + Log::debug(sprintf('%s: - Closing [%s], sent in [%d]',__METHOD__,$this->sending->file_name,$end)); + } + + fclose($this->f); + $this->sending = NULL; + $this->file_pos = 0; + $this->f = NULL; + } + + /** + * Check if we are at the end of the file + * + * @return bool + */ + public function feof(): bool + { + return feof($this->f); + } + + /** + * Open a file for sending + * + * @return bool + * @throws Exception + */ + public function open(): bool + { + Log::debug(sprintf('%s: + Start',__METHOD__)); + + $this->sending = $this->list + ->filter(function($item) { return ($item->action & self::I_SEND) && $item->sent === FALSE; }) + ->first(); + + if (! $this->sending) + throw new Exception('No files to open'); + + $this->file_pos = 0; + $this->start = time(); + + $this->f = fopen($this->sending->file_name,'rb'); + if (! $this->f) { + Log::error(sprintf('%s: ! Unable to open file [%s] for reading',__METHOD__,$this->sending->file_name)); + return FALSE; + } + + Log::info(sprintf('%s: = End - File [%s] opened with size [%d]',__METHOD__,$this->sending->file_name,$this->sending->file_size)); + return TRUE; + } + + /** + * Read bytes of the sending file + * + * @param int $length + * @return string|null + * @throws UnreadableFileException + * @throws Exception + */ + public function read(int $length): ?string + { + if (! $this->f) + throw new Exception('No file open for read'); + + $data = fread($this->f,$length); + $this->file_pos += strlen($data); + Log::debug(sprintf('%s: - Read [%d] bytes, file pos now [%d]',__METHOD__,strlen($data),$this->file_pos)); + + if ($data === FALSE) + throw new UnreadableFileException('Error reading file: '.$this->sending->file_name); + + return $data; + } + + /** + * Seek to a specific position of our file + * + * @param int $pos + * @return bool + * @throws Exception + */ + public function seek(int $pos): bool + { + if (! $this->f) + throw new Exception('No file open for seek'); + + $rc = (fseek($this->f,$pos,SEEK_SET) === 0); + if ($rc) + $this->file_pos = $pos; + + Log::debug(sprintf('%s: - Seeked to [%d]',__METHOD__,$this->file_pos)); + + return $rc; + } +} \ No newline at end of file diff --git a/app/Classes/FileReceive.php b/app/Classes/FileReceive.php new file mode 100644 index 0000000..9e8f351 --- /dev/null +++ b/app/Classes/FileReceive.php @@ -0,0 +1,20 @@ +options = 0; + $this->start_time = Carbon::now(); + $this->ftns = collect(); + $this->ftns_authed = collect(); + } + + /** + * @throws Exception + */ + public function __get($key) + { + switch ($key) { + // Number of AKAs the remote has + case 'aka_num': + return $this->ftns->count(); + + // Number of AKAs we have validated + case 'aka_authed': + return $this->ftns_authed->count(); + + case 'ftn': + return ($x=$this->ftns->first()) ? $x->ftn : 'Unknown'; + + // The nodes password + case 'password': + return ($this->ftns_authed->count() && $x=$this->ftns_authed->first()->sespass) ? $x : '-'; + + // Return how long our session has been connected + case 'session_time': + return Carbon::now()->diffInSeconds($this->start_time); + + case 'system': + case 'sysop': + case 'location': + case 'phone': + case 'flags': + case 'message': + case 'files': + case 'netmail': + // The current session speed + case 'speed': + // The time our session started. + case 'start_time': + case 'software': + return $this->{$key}; + // Client version + case 'ver_major': + return $this->version_major; + case 'ver_minor': + return $this->version_minor; + + default: + throw new Exception('Unknown key: '.$key); + } + } + + /** + * @throws Exception + */ + public function __set($key,$value) + { + switch ($key) { + case 'ftn': + if (! is_object($value) OR ! $value instanceof NodeModel) + throw new Exception('Not a node object: '.(is_object($value) ? get_class($value) : serialize($value))); + + // Ignore any duplicate FTNs that we get + if ($this->ftns->search(function($item) use ($value) { return $item->id === $value->id; }) !== FALSE) { + Log::debug(sprintf('%s: - Ignoring Duplicate FTN [%s]',__METHOD__,$value->ftn)); + break; + } + + $this->ftns->push($value); + break; + + case 'system': + case 'sysop': + case 'location': + case 'phone': + case 'flags': + case 'message': + case 'files': + case 'netmail': + case 'software': + case 'speed': + case 'start_time': + case 'node_time': + + case 'ver_major': + case 'ver_minor': + $this->{$key} = $value; + break; + + default: + throw new Exception('Unknown variable: '.$key); + } + } + + /** + * Authenticate the AKAs that the node provided + * + * @param string $password + * @param string $challenge + * @return int + * @throws Exception + */ + public function auth(string $password,string $challenge=''): int + { + Log::debug(sprintf('%s: + Start [%s]',__METHOD__,$password)); + + // Make sure we havent been here already + if ($this->ftns_authed->count()) + throw new Exception('Already authed'); + + foreach ($this->ftns as $o) { + if (! $o->sespass) + continue; + + // If we have challenge, then we are doing MD5 + $exp_pwd = $challenge ? $this->md5_challenge($o->sespass,$challenge) : $o->sespass; + + if ($exp_pwd === $password) + $this->ftns_authed->push($o); + } + + Log::debug(sprintf('%s: = End [%d]',__METHOD__,$this->ftns_authed->count())); + return $this->ftns_authed->count(); + } + + /** + * When we originate a connection and need to send our MD5 Challenge response + * + * @param string $challenge + * @return string + * @throws Exception + */ + public function get_md5chal(string $challenge): string + { + return $this->md5_challenge($this->password,$challenge); + } + + /** + * Return the remotes BINKP version as a int + * + * @return int + */ + public function get_versionint(): int + { + return $this->ver_major*100+$this->ver_minor; + } + + /** + * Calculate the response to an MD5 challenge, using the nodes password + * + * @param $pwd + * @param $challenge + * @return string + */ + private function md5_challenge($pwd,$challenge): string + { + $x = $pwd.str_repeat(chr(0x00),64-strlen($pwd)); + + return md5($this->str_xor($x,0x5c).md5($this->str_xor($x,0x36).$challenge,true)); + } + + /** + * When we originate a call to a node, we need to store the node we are connecting with in the ftns_authed, so + * authentication proceeds when we send our M_pwd + * + * @param NodeModel $o + */ + public function originate(NodeModel $o): void + { + $this->ftns->push($o); + $this->ftns_authed->push($o); + } + + /** + * Check that our received FTNs match who we called + * + * @return bool + */ + public function originate_check(): bool + { + if ($this->ftns_authed->count() !== 1 || ! $this->ftns->count()) + return FALSE; + + $ftn = $this->ftns_authed->first()->ftn; + + return $this->ftns->search(function($item) use ($ftn) { + return $item->ftn == $ftn; + }) !== FALSE; + } + + public function optionClear(int $key): void + { + $this->options &= ~$key; + } + + public function optionGet(int $key): int + { + return ($this->options & $key); + } + + public function optionSet(int $key): void + { + $this->options |= $key; + } + + private function str_xor(string $string,int $val): string + { + $result = ''; + + for ($i=0;$icomms[$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; + + default: + throw new Exception('Unknown key: '.$key); + } + } + + /** + * 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); + + if ($this->recv->fd) + $this->recv->close(); + } + + /** + * Incoming Protocol session + * + * @param SocketClient $client + * @return int|null + * @throws SocketException + */ + public function onConnect(SocketClient $client): ?int + { + $pid = pcntl_fork(); + + if ($pid == -1) + throw new SocketException(SocketException::CANT_ACCEPT,'Could not fork process'); + + Log::debug(sprintf('%s: = End [%d]',__METHOD__,$pid)); + + // Parent return ready for next connection + return $pid; + } + + protected function optionClear(int $key): void + { + $this->options &= ~$key; + } + + protected function optionGet(int $key): int + { + return ($this->options & $key); + } + + protected function optionSet(int $key): void + { + $this->options |= $key; + } + + /** + * Initialise our Session + * + * @param int $type + * @param SocketClient $client + * @param NodeModel|null $o + * @return int + * @throws Exception + */ + public function session(int $type,SocketClient $client,NodeModel $o=NULL): int + { + Log::debug(sprintf('%s: + Start [%d]',__METHOD__,$type)); + + // This sessions options + $this->options = 0; + $this->session = 0; + + // Our files that we are sending/receive + $this->send = new Send; + $this->recv = new Receive; + + if ($o) { + // Our configuration and initialise values + $this->setup = Setup::findOrFail(self::setup); + + // The node we are communicating with + $this->node = new Node; + + $this->originate = $o->exists; + + // If we are connecting to a node + if ($o->exists) { + $this->node->originate($o); + + } else { + $this->optionSet(self::O_INB); + } + } + + // We are an IP node + $this->optionSet(self::O_TCP); + $this->setClient($client); + + switch ($type) { + /** @noinspection PhpMissingBreakStatementInspection */ + case self::SESSION_AUTO: + Log::debug(sprintf('%s: - Trying EMSI',__METHOD__)); + + $rc = $this->protocol_init(); + if ($rc < 0) { + Log::error(sprintf('%s: ! Unable to start EMSI [%d]',__METHOD__,$rc)); + + return self::S_REDIAL | self::S_ADDTRY; + } + + case self::SESSION_EMSI: + Log::debug(sprintf('%s: - Starting EMSI',__METHOD__)); + $rc = $this->protocol_session(); + + break; + + case self::SESSION_BINKP: + Log::debug(sprintf('%s: - Starting BINKP',__METHOD__)); + $rc = $this->protocol_session(); + + break; + + case self::SESSION_ZMODEM: + Log::debug(sprintf('%s: - Starting ZMODEM',__METHOD__)); + $this->client->speed = SocketClient::TCP_SPEED; + $this->originate = FALSE; + + // @todo While Debugging + $this->send->add('/tmp/aa'); + + return $this->protocol_session(); + + default: + Log::error(sprintf('%s: ! Unsupported session type [%d]',__METHOD__,$type)); + + return self::S_REDIAL | self::S_ADDTRY; + } + + // @todo Unlock outbounds + + // @todo These flags determine when we connect to the remote. + // If the remote indicated that they dont support file requests (NRQ) or temporarily hold them (HRQ) + if (($this->node->optionGet(self::O_NRQ) && (! $this->setup->ignore_nrq)) || $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', + __METHOD__, + $this->node->ftn, + $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', + )); + + // @todo Log to history log in the DB. + //if ($this->node->start_time && $this->setup->cfg('CFG_HISTORY')) {} + + // @todo Optional after session execution event + // if ($this->node->start_time && $this->setup->cfg('CFG_AFTERSESSION')) {} + + // @todo Optional after session includes mail event + // if ($this->node->start_time && $this->setup->cfg('CFG_AFTERMAIL')) {} + + return ($rc & ~self::S_ADDTRY); + } + + /** + * Clear a session bit + * + * @param int $key + */ + protected function sessionClear(int $key): void + { + $this->session &= ~$key; + } + + /** + * Get a session bit + * @param int $key + * @return bool + */ + protected function sessionGet(int $key): bool + { + return ($this->session & $key); + } + + /** + * Set a session bit (with SE_*) + * + * @param int $key + */ + protected function sessionSet(int $key): void + { + $this->session |= $key; + } + + /** + * Set our client that we are communicating with + * + * @param SocketClient $client + */ + private function setClient(SocketClient $client): void + { + $this->client = $client; + } +} \ No newline at end of file diff --git a/app/Classes/Protocol/Binkd.php b/app/Classes/Protocol/Binkd.php new file mode 100644 index 0000000..d535ef0 --- /dev/null +++ b/app/Classes/Protocol/Binkd.php @@ -0,0 +1,1296 @@ +session(self::SESSION_BINKP,$client,(new Node)); + $this->client->close(); + Log::info(sprintf('%s: = End - Connection closed [%s]',__METHOD__,$client->getAddress())); + } + + return NULL; + } + + /** + * @throws Exception + */ + private function binkp_hs(): void + { + Log::debug(sprintf('%s: + Start',__METHOD__)); + + if (! $this->originate && ($this->setup->opt_md&self::O_WANT)) { + $random_key = random_bytes(8); + $this->md_challenge = md5($random_key,TRUE); + $this->msgs(self::BPM_NUL,sprintf('OPT CRAM-MD5-%s',md5($random_key,FALSE))); + } + + $this->msgs(self::BPM_NUL,sprintf('SYS %s',$this->setup->system_name)); + $this->msgs(self::BPM_NUL,sprintf('ZYZ %s',$this->setup->sysop)); + $this->msgs(self::BPM_NUL,sprintf('LOC %s',$this->setup->location)); + $this->msgs(self::BPM_NUL,sprintf('NDL %d,TCP,BINKP',$this->client->speed)); + $this->msgs(self::BPM_NUL,sprintf('TIME %s',Carbon::now()->toRfc2822String())); + $this->msgs(self::BPM_NUL, + sprintf('VER %s-%s %s/%s',config('app.name'),$this->setup->version,self::BP_PROT,self::BP_VERSION)); + + if ($this->originate) { + $this->msgs(self::BPM_NUL, + sprintf('OPT%s%s%s%s%s%s', + ($this->setup->opt_nda) ? ' NDA' : '', + ($this->setup->opt_nr&self::O_WANT) ? ' NR' : '', + ($this->setup->opt_nd&self::O_THEY) ? ' ND' : '', + ($this->setup->opt_mb&self::O_WANT) ? ' MB' : '', + ($this->setup->opt_cr&self::O_WE) ? ' CRYPT' : '', + ($this->setup->opt_cht&self::O_WANT) ? ' CHAT' : '')); + } + + // If we are originating, we'll show the remote our address in the same network + // @todo Implement hiding our AKAs not in this network. + if ($this->originate) + $this->msgs(self::BPM_ADR,join(' ',$this->setup->nodes->pluck('ftn')->toArray())); + } + + /** + * @return int + */ + private function binkp_hsdone(): int + { + Log::debug(sprintf('%s: + Start',__METHOD__)); + + if ($this->setup->opt_nd == self::O_WE || $this->setup->opt_nd == self::O_THEY) + $this->setup->opt_nd = self::O_NO; + + if (! $this->setup->phone) + $this->setup->phone = '-Unpublished-'; + + if ( ! $this->optionGet(self::O_PWD) || $this->setup->opt_md != self::O_YES) + $this->setup->opt_cr = self::O_NO; + + if (($this->setup->opt_cr&self::O_WE ) && ($this->setup->opt_cr&self::O_THEY)) { + dump('Enable crypting messages'); + + /* + $this->setup->opt_cr = O_YES; + if ( bp->to ) { + init_keys( bp->keys_out, $this->node->password ); + init_keys( bp->keys_in, "-" ); + keys = bp->keys_in; + } else { + init_keys( bp->keys_in, $this->node->password ); + init_keys( bp->keys_out, "-" ); + keys = bp->keys_out; + } + for( p = $this->node->password; *p; p++ ) { + update_keys( keys, (int) *p ); + } + */ + } + + // @todo Implement max incoming sessions and max incoming session for the same node + + // We have no mechanism to support chat + if ($this->setup->opt_cht&self::O_WANT) + Log::warning(sprintf('%s: - We cant do chat',__METHOD__)); + + if ($this->setup->opt_nd&self::O_WE || ($this->originate && ($this->setup->opt_nr&self::O_WANT) && $this->node->get_versionint() > 100)) + $this->setup->opt_nr |= self::O_WE; + + if (($this->setup->opt_cht&self::O_WE ) && ($this->setup->opt_cht&self::O_WANT)) + $this->setup->opt_cht = self::O_YES; + + $this->setup->opt_mb = ($this->node->get_versionint() > 100 || ($this->setup->opt_mb&self::O_WE)) ? self::O_YES : self::O_NO; + + if ($this->node->get_versionint() > 100) + $this->sessionClear(self::SE_DELAYEOB); + + $this->mib = 0; + $this->sessionClear(self::SE_INIT); + + Log::info(sprintf('%s: - Session: BINKP/%d.%d - NR:%d, ND:%d, MD:%d, MB:%d, CR:%d, CHT:%d', + __METHOD__, + $this->node->ver_major, + $this->node->ver_minor, + $this->setup->opt_nr, + $this->setup->opt_nd, + $this->setup->opt_md, + $this->setup->opt_mb, + $this->setup->opt_cr, + $this->setup->opt_cht + )); + + return 1; + } + + private function binkp_init(): int + { + Log::debug(sprintf('%s: + Start',__METHOD__)); + + $this->sessionSet(self::SE_INIT); + + $this->is_msg = -1; + $this->mib = 0; + $this->error = 0; + $this->mqueue = collect(); + + $this->rx_ptr = 0; + $this->rx_size = -1; + + $this->tx_buf = ''; + $this->tx_left = 0; + $this->tx_ptr = 0; + + for ($x=0;$xsetup->binkp_options);$x++) { + switch (strtolower($this->setup->binkp_options[$x])) { + case 'p': /* Force password digest */ + $this->setup->opt_md |= self::O_NEED; + + case 'm': /* Want password digest */ + $this->setup->opt_md |= self::O_WANT; + break; + + case 'c': /* Can crypt */ + $this->setup->opt_cr |= self::O_WE; + break; + + case 'd': /* No dupes mode */ + $this->setup->opt_nd |= self::O_NO; /*mode?O_THEY:O_NO;*/ + + case 'r': /* Non-reliable mode */ + $this->setup->opt_nr |= ($this->originate ? self::O_WANT : self::O_NO); + break; + + case 'b': /* Multi-batch mode */ + $this->setup->opt_mb |= self::O_WANT; + break; + + case 't': /* Chat - not implemented */ + //$this->setup->opt_cht |= self::O_WANT; + break; + + default: + Log::error(sprintf('%s: ! Error - Unknown BINKP option [%s]',__METHOD__,$this->setup->binkp_options[$x])); + } + } + + return self::OK; + } + + /** + * @throws Exception + */ + private function binkp_recv(): int + { + Log::debug(sprintf('%s: + Start',__METHOD__)); + + $buf = ''; + $blksz = $this->rx_size == -1 ? BinkpMessage::BLK_HDR_SIZE : $this->rx_size; + + if ($blksz !== 0) { + try { + $buf = $this->client->read(0,$blksz-$this->rx_ptr); + + } catch (SocketException $e) { + if ($e->getCode() == 11) { + // @todo We maybe should count these and abort if there are too many? + Log::debug(sprintf('%s: - Socket EAGAIN',__METHOD__)); + return 1; + } + + $this->socket_error = $e->getMessage(); + $this->error = 1; + + return 0; + } + + if (strlen($buf) == 0) { + // @todo Check that this is correct. + Log::debug(sprintf('%s: - Was the socket closed by the remote?',__METHOD__)); + $this->error = -2; + return 0; + } + + /* + if ($this->setup->opt_cr == self::O_YES ) { + //decrypt_buf( (void *) &bp->rx_buf[bp->rx_ptr], (size_t) readsz, bp->keys_in ); + } + */ + } + + $this->rx_ptr += strlen($buf); + + /* Received complete block */ + if ($this->rx_ptr == $blksz) { + /* Header */ + if ($this->rx_size == -1 ) { + $this->is_msg = ord(substr($buf,0,1)) >> 7; + $this->rx_size = ((ord(substr($buf,0,1))&0x7f) << 8 )+ord(substr($buf,1,1)); + $this->rx_ptr = 0; + + Log::debug(sprintf('%s: - HEADER, is_msg [%d]',__METHOD__,$this->is_msg)); + + if ($this->rx_size == 0) + goto ZeroLen; + + $rc = 1; + + /* Next block */ + } else { + ZeroLen: + Log::debug(sprintf('%s: - NEXT BLOCK, is_msg [%d]',__METHOD__,$this->is_msg)); + + if ($this->is_msg) { + $this->mib++; + + /* Handle zero length block */ + if ($this->rx_size == 0 ) { + Log::debug(sprintf('%s: - Zero length msg - dropped',__METHOD__)); + $this->rx_size = -1; + $this->rx_ptr = 0; + + return 1; + } + + Log::debug(sprintf('%s: - BUFFER [%d]',__METHOD__,strlen($buf))); + + $rc = ord(substr($buf,0,1)); + + if ($rc > self::BPM_MAX) { + Log::error(sprintf('%s: ! Unknown Message [%s] (%d)',__METHOD__,$buf,strlen($buf))); + $rc = 1; + + } else { + //DEBUG(('B',2,"rcvd %s '%s'%s",mess[rc],bp->rx_buf + 1,CRYPT(bps))); //@todo CRYPT + $data = substr($buf,1); + switch ($rc) { + case self::M_ADR: + $rc = $this->M_adr($data); + break; + + case self::M_EOB: + $rc = $this->M_eob($data); + break; + + case self::M_NUL: + $rc = $this->M_nul($data); + break; + + case self::M_PWD: + $rc = $this->M_pwd($data); + break; + + case self::M_ERR: + $rc = $this->M_err($data); + break; + + case self::M_FILE: + $rc = $this->M_file($data); + break; + + case self::M_GET: + $rc = $this->M_get($data); + break; + + case self::M_GOTSKIP: + $rc = $this->M_gotskip($data); + break; + + case self::M_OK: + $rc = $this->M_ok($data); + break; + + case self::M_CHAT: + $rc = $this->M_chat($data); + break; + + default: + Log::error(sprintf('%s: ! Command not implemented [%d]',__METHOD__,$rc)); + $rc = 1; + } + } + + } else { + $tmp = sprintf('%s %lu %lu',$this->recv->name,$this->recv->size,$this->recv->mtime); // @todo move to Receive? + + if ($this->recv->fd) { + try { + $rc = $this->recv->write($buf); + + } catch (Exception $e) { + Log::error(sprintf('%s: ! %s',__METHOD__,$e->getMessage())); + $this->recv->close(); + $this->msgs(self::BPM_SKIP,$tmp); + $rc = 1; + } + + if ($this->recv->filepos == $this->recv->size) { + Log::debug(sprintf('%s: - Finished receiving file [%s] with size [%d]',__METHOD__,$this->recv->name,$this->recv->size)); + + $this->recv->close(); + $this->msgs(self::BPM_GOT,$tmp); + $rc = 1; + } + + } else { + Log::critical(sprintf('%s: - Ignoring data block', __METHOD__)); + $rc = 1; + } + } + + $this->rx_ptr = 0; + $this->rx_size = -1; + } + + } else { + $rc = 1; + } + + Log::debug(sprintf('%s: = End [%d]',__METHOD__,$rc)); + return $rc; + } + + /** + * @throws Exception + */ + private function binkp_send(): int + { + Log::debug(sprintf('%s: + Start - tx_left [%d]',__METHOD__,$this->tx_left)); + + if ($this->tx_left == 0 ) { /* tx buffer is empty */ + $this->tx_ptr = $this->tx_left = 0; + + Log::debug(sprintf('%s: - Msgs [%d]',__METHOD__,$this->mqueue->count())); + + if ($this->mqueue->count()) { /* there are unsent messages */ + while ($msg = $this->mqueue->shift()) { + if (($msg->len+$this->tx_left) > self::MAX_BLKSIZE) { + break; + } + + $this->tx_buf .= $msg->msg; + $this->tx_left += $msg->len; + } + + } elseif ($this->sessionGet(self::SE_SENDFILE) && $this->send->fd && (! $this->sessionGet(self::SE_WAITGET))) { + $data = ''; + + try { + $data = $this->send->read(self::BP_BLKSIZE); + + } catch (UnreadableFileException) { + $this->send->close(FALSE); + $this->sessionClear(self::SE_SENDFILE); + + } catch (Exception $e) { + Log::error(sprintf('%s: ! Unexpected ERROR [%s]',__METHOD__,$e->getMessage())); + } + + if ($data) { + $this->tx_buf .= BinkpMessage::mkheader(strlen($data)); + $this->tx_buf .= $data; + + /* + if ($this->setup->opt_cr == self::O_YES) { + encrypt_buf($this->tx_buf,($data + BinkpMessage::BLK_HDR_SIZE),$this->keys_out); + } + */ + + $this->tx_left = strlen($data)+BinkpMessage::BLK_HDR_SIZE; + } + + // @todo should this be less than BP_BLKSIZE? Since a read could return a blocksize and it could be the end of the file? + if ($data < self::BP_BLKSIZE && $this->send->filepos == $this->send->size) { + $this->sessionSet(self::SE_WAITGOT); + $this->sessionClear(self::SE_SENDFILE); + } + } + + } else { + try { + $rc = $this->client->send(substr($this->tx_buf,$this->tx_ptr,$this->tx_left),self::BP_TIMEOUT); + + } catch (Exception $e) { + if ($e->getCode() == 11) + return 1; + + $this->socket_error = $e->getMessage(); + Log::error(sprintf('%s: ! Error [%s]',__METHOD__,$e->getMessage())); + return 0; + } + + $this->tx_ptr += $rc; + $this->tx_left -= $rc; + + if (! $this->tx_left) { + $this->tx_buf = ''; + $this->tx_ptr = 0; + } + } + + Log::debug(sprintf('%s: = End [1]',__METHOD__)); + return 1; + } + + private function file_parse(string $str): ?array + { + Log::debug(sprintf('%s: + Start [%s]',__METHOD__,$str)); + + $name = $this->strsep($str,' '); + $size = $this->strsep($str,' '); + $time = $this->strsep($str,' '); + $offs = $this->strsep($str,' '); + + if ($name && $size && $time) { + return [ + 'file'=>['name'=>$name,'size'=>$size,'mtime'=>$time], + 'offs'=>$offs + ]; + } + + return NULL; + } + + /** + * Add a BINKP control message to the queue + * + * @param string $id + * @param string $msg_body + * @return void + */ + private function msgs(string $id,string $msg_body): void + { + Log::debug(sprintf('%s: + Start [%s]',__METHOD__,$msg_body)); + + $this->mqueue->push(new BinkpMessage($id,$msg_body)); + + /* + if ($this->setup->opt_cr == self::O_YES) { + //$this->encrypt_buf($this->bps->mqueue[$this->nmsgs]->msg,$this->bps->mqueue[$this->nmsgs]->len,$this->bps->keys_out); + } + */ + + $this->mib++; + } + + /** + * @throws Exception + */ + private function M_adr(string $buf): int + { + Log::debug(sprintf('%s: + Start [%s]',__METHOD__,$buf)); + + $buf = $this->skip_blanks($buf); + $rc = 0; + + while(($rem_aka = $this->strsep($buf,' '))) { + Log::debug(sprintf('%s: - Parsing AKA [%s]',__METHOD__,$rem_aka)); + + try { + if (! ($o = Node::findFTN($rem_aka))) { + Log::debug(sprintf('%s: ? AKA is UNKNOWN [%s]',__METHOD__,$rem_aka)); + + continue; + } + + } catch (Exception) { + Log::error(sprintf('%s: ! AKA is INVALID [%s]',__METHOD__,$rem_aka)); + + $this->msgs(self::BPM_ERR,sprintf('Bad address %s',$rem_aka)); + $this->rc = self::S_FAILURE; + + return 0; + } + + // Check if the remote has our AKA + if ($this->setup->nodes->pluck('ftn')->search($o->ftn) !== FALSE) { + Log::error(sprintf('%s: ! AKA is OURS [%s]',__METHOD__,$o->ftn)); + + $this->msgs(self::BPM_ERR,sprintf('Sorry that is my AKA [%s]',$o->ftn)); + $this->rc = self::S_FAILURE; + + return 0; + } + + // @todo lock nodes + $this->node->ftn = $o; + + // @todo Find files for node + $this->send->add('/tmp/aa'); + + Log::info(sprintf('%s: = Node has [%lu] mail and [%lu] files - [%lu] items',__METHOD__,$this->send->mail_size,$this->send->file_size,$this->send->total_count)); + + $rc = $this->node->aka_num; + } + + if ($rc == 0) { + Log::error(sprintf('%s: ! All AKAs [%d] are busy',__METHOD__,$this->node->aka_num)); + + $this->msgs( self::BPM_BSY,'All AKAs are busy'); + $this->rc = ($this->originate ? self::S_REDIAL|self::S_ADDTRY : self::S_BUSY); + + return 0; + } + + if ($this->originate) { + if (! $this->node->originate_check()) { + Log::error(sprintf('%s: ! We didnt get who we called?',__METHOD__)); + + $this->msgs( self::BPM_ERR,'Sorry, you are not who I expected'); + $this->rc = self::S_FAILURE|self::S_ADDTRY; + + return 0; + } + + $this->msgs(self::BPM_NUL,sprintf('TRF %lu %lu',$this->send->mail_size,$this->send->file_size)); + + if ($this->md_challenge) { + $this->msgs(self::BPM_PWD,sprintf('CRAM-MD5-%s',$this->node->get_md5chal($this->md_challenge))); + + } elseif ($this->setup->opt_md == self::O_YES ) { + $this->msgs(self::BPM_ERR,'Can\'t use plaintext password'); + $this->rc = self::S_FAILURE|self::S_ADDTRY; + + return 0; + + } else { + $this->msgs(self::BPM_PWD,$this->node->password); + } + } + + if (! $this->node->aka_num) + $this->optionClear(self::O_PWD); + else + $this->optionSet(self::O_PWD); + + // If we are not the originator, we'll show our addresses in common. + // @todo make this an option to hideAKAs or not + if (! $this->originate) + $this->msgs(self::BPM_ADR,join(' ',$this->setup->nodes->pluck('ftn')->toArray())); + + return 1; + } + + private function M_chat(string $buf): int + { + Log::debug(sprintf('%s: + Start [%s]',__METHOD__,$buf)); + + if ($this->setup->opt_cht == self::O_YES) { + Log::error(sprintf('%s: - We cannot do chat',__METHOD__)); + + } else { + Log::error(sprintf('%s: - We got a chat message, but chat is disabled',__METHOD__)); + } + + return 1; + } + + /** + * @throws Exception + */ + private function M_eob(string $buf): int + { + Log::debug(sprintf('%s: + Start [%s]',__METHOD__,$buf)); + + if ($this->recv->fd) + $this->recv->close(); + + $this->sessionSet(self::SE_RECVEOB); + $this->sessionClear(self::SE_DELAYEOB); + + if (! $this->send->total_count && $this->sessionGet(self::SE_NOFILES)) { + // @todo See if we need to send anything else, based on what we just recevied + if ($this->send->total_count) + $this->sessionClear(self::SE_NOFILES); + } + + return 1; + } + + /** + * @throws Exception + */ + private function M_err(string $buf): int + { + Log::debug(sprintf('%s: + Start [%s]',__METHOD__,$buf)); + + $this->error_close(); + $this->rc = ($this->originate ? self::S_REDIAL|self::S_ADDTRY : self::S_BUSY); + + return 0; + } + + /** + * @throws Exception + */ + private function M_file(string $buf): int + { + Log::debug(sprintf('%s: + Start [%s]',__METHOD__,$buf)); + + if ($this->sessionGet(self::SE_SENTEOB|self::SE_RECVEOB)) + $this->sessionClear(self::SE_SENTEOB); + + $this->sessionClear(self::SE_RECVEOB); + + if ($this->recv->fd) + $this->recv->close(); + + if (! $file = $this->file_parse($buf)) { + Log::error(sprintf('%s: - UNPARSABLE file info [%s]',__METHOD__,$buf)); + $this->msgs(self::BPM_ERR,sprintf('M_FILE: unparsable file info: "%s"',$buf)); + + if ($this->sessionGet(self::SE_SENDFILE)) + $this->send->close(FALSE); + + $this->rc = ($this->originate ? self::S_REDIAL|self::S_ADDTRY : self::S_BUSY); + + return 0; + } + + // In NR mode, when we got -1 for the file offsite, the reply to our get will confirm our requested offset. + if ($this->recv->name && ! strncasecmp(Arr::get($file,'file.name'),$this->recv->name,self::MAX_PATH) + && $this->recv->mtime == Arr::get($file,'file.mtime') + && $this->recv->size == Arr::get($file,'file.size') + && $this->recv->filepos == $file['offs']) + { + $this->recv->open($file['offs']<0); + + return 1; + } + + $this->recv->new($file['file']); + + try { + switch ($this->recv->open($file['offs']<0)) { + case self::FOP_ERROR: + Log::error(sprintf('%s: ! File Error',__METHOD__)); + + case self::FOP_SUSPEND: + Log::info(sprintf('%s: - File Suspended',__METHOD__)); + $this->msgs(self::BPM_SKIP, sprintf('%s %lu %lu',$this->recv->name,$this->recv->size,$this->recv->mtime)); + + break; + + case self::FOP_SKIP: + Log::info(sprintf('%s: - File Skipped',__METHOD__)); + $this->msgs(self::BPM_GOT,sprintf('%s %lu %lu',$this->recv->name,$this->recv->size,$this->recv->mtime)); + + break; + + case self::FOP_OK: + Log::debug(sprintf('%s: - Getting file from [%d]',__METHOD__,$file['offs'])); + + if ($file['offs'] != -1) { + if (! ($this->setup->opt_nr&self::O_THEY)) { + $this->setup->opt_nr |= self::O_THEY; + } + + break; + } + + case self::FOP_CONT: + Log::debug(sprintf('%s: - Continuing file [%s] (%lu %lu %ld)',__METHOD__, + $this->recv->name,$this->recv->size,$this->recv->mtime,$file['offs'])); + + $this->msgs(self::BPM_GET,sprintf('%s %lu %lu %ld', + $this->recv->name, + $this->recv->size, + $this->recv->mtime, + ($file['offs'] < 0) ? 0 : $file['offs'])); + + break; + } + + } catch (Exception $e) { + Log::error(sprintf('%s: ! File Open Error [%s]',__METHOD__,$e->getMessage())); + + $this->msgs(self::BPM_SKIP,sprintf('%s %lu %lu',$this->recv->name,$this->recv->size,$this->recv->mtime)); + } + + return 1; + } + + /** + * @throws Exception + */ + private function M_get(string $buf): int + { + Log::debug(sprintf('%s: + Start [%s]',__METHOD__,$buf)); + + if ($file = $this->file_parse($buf)) { + if ($this->sessionGet(self::SE_SENDFILE) + && $this->send->sendas + && ! strncasecmp(Arr::get($file,'file.name'),$this->send->sendas,self::MAX_PATH) + && $this->send->mtime == Arr::get($file,'file.mtime') + && $this->send->size == Arr::get($file,'file.size')) + { + if (! $this->send->seek($file['offs'])) { + Log::error(sprintf('%s: ! Cannot send file from requested offset [%d]',__METHOD__,$file['offs'])); + + $this->msgs(self::BPM_ERR,'Can\'t send file from requested offset'); + $this->send->close(FALSE); + $this->sessionClear(self::SE_SENDFILE); + + } else { + $this->sessionClear(self::SE_WAITGET); + $this->msgs(self::BPM_FILE,sprintf('%s %lu %ld %lu',$this->send->sendas,$this->send->size,$this->send->mtime,$file['offs'])); + } + + } else { + Log::error(sprintf('%s: ! M_got for unknown file [%s]',__METHOD__,$buf)); + } + + } else { + Log::error(sprintf('%s: - UNPARSABLE file info [%s]',__METHOD__,$buf)); + } + + return 1; + } + + /** + * M_GOT/M_SKIP commands + * + * @param string $buf + * @return int + * @throws Exception + */ + private function M_gotskip(string $buf):int + { + Log::debug(sprintf('%s: + Start [%s]',__METHOD__,$buf)); + + if ($file = $this->file_parse($buf)) { + if ($this->send->sendas + && ! strncasecmp(Arr::get($file,'file.name'),$this->send->sendas,self::MAX_PATH) + && $this->send->mtime == Arr::get($file,'file.mtime') + && $this->send->size == Arr::get($file,'file.size')) + { + if ($this->sessionGet(self::SE_SENDFILE)) { + $this->send->close(TRUE); + $this->sessionClear(self::SE_SENDFILE); + + return 1; + } + + if ($this->sessionGet(self::SE_WAITGOT)) { + $this->sessionClear(self::SE_WAITGOT); + $this->send->close(TRUE); + + } else { + Log::error(sprintf('%s: ! M_got[skip] for unknown file [%s]',__METHOD__,$buf)); + } + } + + } else { + Log::error(sprintf('%s: - UNPARSABLE file info [%s]',__METHOD__,$buf)); + } + + return 1; + } + + /** + * @throws Exception + */ + private function M_nul(string $buf): int + { + Log::debug(sprintf('%s: + Start [%s]',__METHOD__,$buf)); + + if (! strncmp($buf,'SYS ',4)) { + $this->node->system = $this->skip_blanks(substr($buf,4)); + + } elseif (! strncmp($buf, 'ZYZ ',4)) { + $this->node->sysop = $this->skip_blanks(substr($buf,4)); + + } elseif (! strncmp($buf,'LOC ',4)) { + $this->node->location = $this->skip_blanks(substr($buf,4)); + + } elseif (! strncmp($buf,'NDL ',4)) { + $data = $this->skip_blanks(substr($buf,4)); + $comma = strpos($data,','); + $spd = substr($data,0,$comma); + + if ($comma) + $this->node->flags = substr($data,$comma+1); + + if ($spd >= 300) { + $this->client->speed = $spd; + + } else { + $comma = $this->skip_blanks(substr($buf,4)); + $c = 0; + while (($x=substr($comma,$c,1)) && is_numeric($x)) + $c++; + + $comma = substr($comma,0,$c); + + if (! $comma) { + $this->client->speed = SocketClient::TCP_SPEED; + + } elseif (strtolower(substr($comma,$c+1,1)) == 'k') { + $this->client->speed = $spd * 1024; + + } elseif (strtolower(substr($comma,$c+1,1)) == 'm') { + $this->client->speed = $spd * 1024 * 1024; + + } else { + $this->client->speed = SocketClient::TCP_SPEED; + } + } + + } elseif (! strncmp($buf,'TIME ',5)) { + $this->node->node_time = $this->skip_blanks(substr($buf,5)); + + } elseif (! strncmp($buf,'VER ',4)) { + $data = $this->skip_blanks(substr($buf,4)); + $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]; + + } elseif (! strncmp($buf,'TRF ',4)) { + $data = $this->skip_blanks(substr($buf,4)); + $matches = []; + preg_match('/^([0-9]+)\s+([0-9]+)$/',$data,$matches); + + $this->node->netmail = isset($matches[1]) ?: 0; + $this->node->files = isset($matches[2]) ?: 0; + + if ($this->node->netmail + $this->node->files) + $this->sessionSet(self::SE_DELAYEOB); + + } elseif (! strncmp($buf,'FREQ',4)) { + $this->sessionSet(self::SE_DELAYEOB); + + } elseif (! strncmp($buf,'PHN ',4)) { + $this->node->phone = $this->skip_blanks(substr($buf,4)); + + } elseif (! strncmp($buf,'OPM ',4)) { + $this->node->message = $this->skip_blanks(substr($buf,4)); + + } elseif (! strncmp($buf,'OPT ',4)) { + $data = $this->skip_blanks(substr($buf,4)); + + while ($data && ($p = $this->strsep($data,' '))) { + + if (! strcmp($p,'NR')) { + $this->setup->opt_nr |= self::O_WE; + + } elseif (! strcmp($p,'MB')) { + $this->setup->opt_mb |= self::O_WE; + + } elseif (! strcmp($p,'ND')) { + $this->setup->opt_nd |= self::O_WE; + + } elseif (! strcmp($p,'NDA')) { + $this->setup->opt_nd |= self::O_EXT; + + } elseif (! strcmp($p,'CHAT')) { + $this->setup->opt_cht |= self::O_WE; + + } elseif (! strcmp($p,'CRYPT')) { + $this->setup->opt_cr |= self::O_THEY; + + } elseif (! strncmp($p,'CRAM-MD5-',9) && $this->originate && $this->setup->opt_md) { + if (($x=strlen(substr($p,9))) > 64 ) { + Log::error(sprintf('%s: - Got TOO LONG [%d] challenge string',__METHOD__,$x)); + + } else { + $this->md_challenge = hex2bin(substr($p,9)); + + if ($this->md_challenge) + $this->setup->opt_md |= self::O_THEY; + } + + if (($this->setup->opt_md&(self::O_THEY|self::O_WANT)) == (self::O_THEY|self::O_WANT)) + $this->setup->opt_md = self::O_YES; + + } else { /* if ( strcmp( p, "GZ" ) || strcmp( p, "BZ2" ) || strcmp( p, "EXTCMD" )) */ + Log::warning(sprintf('%s: - Got UNSUPPORTED option [%s]',__METHOD__,$p)); + } + + $data = substr($data,strpos($data,' ')); + } + + } else { + Log::warning(sprintf('%s: - Got UNKNOWN NUL [%s]',__METHOD__,$buf)); + } + + return 1; + } + + /** + * @throws Exception + */ + private function M_ok(string $buf): int + { + Log::debug(sprintf('%s: + Start [%s]',__METHOD__,$buf)); + + if (! $this->originate) { + Log::error(sprintf('%s: ! UNEXPECTED M_OK [%s] from remote on incoming call',__METHOD__,$buf)); + + $this->rc = self::S_FAILURE; + return 0; + } + + $buf = $this->skip_blanks($buf); + + if (($this->optionGet(self::O_PWD)) && $buf) { + while (($t = $this->strsep($buf," \t"))) + if (strcmp($t,'non-secure') == 0) { + Log::debug(sprintf('%s: - Non Secure',__METHOD__)); + + $this->setup->opt_cr = self::O_NO; + $this->optionClear(self::O_PWD); + + break; + } + } + + Log::debug(sprintf('%s: = End',__METHOD__)); + return $this->binkp_hsdone(); + } + + /** + * @throws Exception + */ + private function M_pwd(string $buf): int + { + Log::debug(sprintf('%s: + Start [%s]',__METHOD__,$buf)); + + $buf = $this->skip_blanks($buf); + $have_CRAM = !strncasecmp($buf,'CRAM-MD5-',9); + $have_pwd = $this->optionGet(self::O_PWD); + + if ($this->originate) { + Log::error(sprintf('%s: ! Unexpected password [%s] from remote on OUTGOING call',__METHOD__,$buf)); + + $this->rc = self::S_FAILURE; + return 0; + } + + if ($this->md_challenge) { + if ($have_CRAM) { + // Loop to match passwords + $this->node->auth(substr($buf,9),$this->md_challenge); + $this->setup->opt_md |= self::O_THEY; + + } elseif ($this->setup->opt_md&self::O_NEED) { + Log::error(sprintf('%s: ! Remote doesnt support MD5',__METHOD__)); + + $this->msgs( self::BPM_ERR,'You must support MD5'); + $this->rc = self::S_FAILURE; + + return 0; + } + } + + if (! $this->md_challenge || (! $have_CRAM && ! ($this->setup->opt_md&self::O_NEED))) { + // Loop to match passwords + $this->node->auth($buf); + } + + if ($have_pwd) { + // If no passwords matched (ie: aka_authed is 0) + if (! $this->node->aka_authed) { + Log::error(sprintf('%s: ! Bad password [%s]',__METHOD__,$buf)); + + $this->msgs(self::BPM_ERR,'Security violation'); + $this->optionSet(self::O_BAD); + $this->rc = self::S_FAILURE; + + return 0; + } + + } elseif (! $this->node->aka_authed) { + Log::notice(sprintf('%s: - Remote proposed password for us [%s]',__METHOD__,$buf)); + } + + if (($this->setup->opt_md&(self::O_THEY|self::O_WANT )) == (self::O_THEY|self::O_WANT)) + $this->setup->opt_md = self::O_YES; + + if (!$have_pwd || $this->setup->opt_md != self::O_YES) + $this->setup->opt_cr = self::O_NO; + + $tmp = sprintf('%s%s%s%s%s%s', + ($this->setup->opt_nr&self::O_WANT) ? ' NR' : '', + ($this->setup->opt_nd&self::O_THEY) ? ' ND' : '', + ($this->setup->opt_mb&self::O_WANT) ? ' MB' : '', + ($this->setup->opt_cht&self::O_WANT) ? ' CHAT' : '', + ((! ($this->setup->opt_nd&self::O_WE)) != (! ($this->setup->opt_nd&self::O_THEY))) ? ' NDA': '', + (($this->setup->opt_cr&self::O_WE) && ($this->setup->opt_cr&self::O_THEY )) ? ' CRYPT' : ''); + + if (strlen($tmp)) + $this->msgs(self::BPM_NUL,sprintf('OPT%s',$tmp)); + + $this->msgs(self::BPM_NUL,sprintf('TRF %lu %lu',$this->send->mail_size,$this->send->file_size)); + $this->msgs(self::BPM_OK,sprintf('%ssecure',$have_pwd ? '' : 'non-')); + + return $this->binkp_hsdone(); + } + + protected function protocol_init(): int + { + // Not Used + } + + /** + * Setup our BINKP session + * + * @return int + * @throws Exception + */ + protected function protocol_session(): int + { + Log::debug(sprintf('%s: + Start',__METHOD__)); + + if ($this->binkp_init() != self::OK) + return $this->originate ? (self::S_REDIAL|self::S_ADDTRY) : self::S_FAILURE; + + $this->binkp_hs(); + + while (TRUE) { + if (! $this->sessionGet(self::SE_INIT|self::SE_SENDFILE|self::SE_SENTEOB|self::SE_NOFILES) && ! $this->send->fd) { + // Open our next file to send + if ($this->send->total_count && ! $this->send->fd) + $this->send->open(); + + if ($this->send->fd) { + $this->sessionSet(self::SE_SENDFILE); + + if ($this->setup->opt_nr&self::O_WE) { + $this->sessionSet(self::SE_WAITGET); + + Log::debug(sprintf('%s: - Waiting for M_GET',__METHOD__)); + } + + $this->msgs(self::BPM_FILE, + sprintf('%s %lu %lu %ld',$this->send->sendas,$this->send->size,$this->send->mtime,$this->sessionGet(self::SE_WAITGET) ? -1 : 0)); + + $this->sessionClear(self::SE_SENTEOB); + + } else { + $this->sessionSet(self::SE_NOFILES); + } + } + + if (! $this->sessionGet(self::SE_INIT|self::SE_WAITGOT|self::SE_SENTEOB|self::SE_DELAYEOB) && $this->sessionGet(self::SE_NOFILES)) { + $this->msgs(self::BPM_EOB,''); + $this->sessionSet(self::SE_SENTEOB); + } + + $this->rc = self::S_OK; + + if ($this->sessionGet(self::SE_SENTEOB) && $this->sessionGet(self::SE_RECVEOB)) { + if ($this->mib < 3 || $this->node->get_versionint() <= 100) { + break; + } + + $this->mib = 0; + $this->sessionClear(self::SE_RECVEOB|self::SE_SENTEOB); + } + + $wd = ($this->mqueue->count() || $this->tx_left || ($this->sessionGet(self::SE_SENDFILE) && $this->send->fd && ! $this->sessionGet(self::SE_WAITGET))); + $rd = TRUE; + + try { + // @todo we need to catch a timeout if there are no reads/writes + $rc = $this->client->ttySelect($rd,$wd,self::BP_TIMEOUT); + + } catch (Exception) { + $this->error_close(); + $this->error = -2; + + break; + } + + $this->rc = ($this->originate ? (self::S_REDIAL|self::S_ADDTRY) : self::S_BUSY); + + if ($rc == 0) { + $this->error_close(); + $this->error = -1; + + break; + } + + if ($rd && ! $this->binkp_recv()) + break; + + if (($this->mqueue->count() || $wd) && ! $this->binkp_send()) + break; + } + + if ($this->error == -1) + Log::error(sprintf('%s: ! TIMEOUT',__METHOD__)); + + elseif ($this->error > 0) + Log::error(sprintf('%s: ! Got ERROR [%d]',__METHOD__,$this->socket_error)); + + while (! $this->error) { + try { + $buf = $this->client->read(0,self::MAX_BLKSIZE); + + } catch (Exception $e) { + if ($e->getCode() !== 11) { + Log::debug(sprintf('%s: ? Got Exception [%d] (%s)',__METHOD__,$e->getCode(),$e->getMessage())); + + $this->error = 1; + } + + break; + } + + if (strlen($buf) == 0) + break; + + Log::warning(sprintf('%s: - Purged (%s) [%d] bytes from input stream',__METHOD__,serialize($buf),strlen($buf))); + } + + while (! $this->error && ($this->mqueue->count() || $this->tx_left) && $this->binkp_send()); + + return $this->rc; + } + + /** + * Strip blanks at the beginning of a string + * + * @param string $str + * @return string + * @throws Exception + */ + private function skip_blanks(string $str): string + { + $c = 0; + + if ($str != NULL) + while ($this->isSpace(substr($str,$c,1))) + $c++; + + return substr($str,$c); + } + + /** + * Return the string delimited by char and shorten the input to the remaining characters + * + * @param string $str + * @param string $char + * @return string + */ + private function strsep(string &$str,string $char): string + { + $return = strstr($str,$char,TRUE) ?: $str; + $str = substr($str,strlen($return)+strlen($char)); + + return $return; + } + + /** + * Check if the string is a space + * + * @param string $str + * @return bool + * @throws Exception + */ + private function isSpace(string $str):bool + { + if (strlen($str) > 1) + throw new Exception('String is more than 1 char'); + + return $str && in_array($str,[' ',"\n","\r","\v","\f","\t"]); + } +} + diff --git a/app/Classes/Protocol/BinkpMessage.php b/app/Classes/Protocol/BinkpMessage.php new file mode 100644 index 0000000..b541199 --- /dev/null +++ b/app/Classes/Protocol/BinkpMessage.php @@ -0,0 +1,43 @@ +id = $id; + $this->body = $body; + } + + public function __get($key) + { + switch ($key) { + case 'len': + return strlen($this->body)+1+self::BLK_HDR_SIZE; + + case 'msg': + $buf = self::mkheader((strlen($this->body)+1) | 0x8000); + $buf .= chr($this->id); + $buf .= $this->body; + + return $buf; + + default: + throw new \Exception('Unknown key :'.$key); + } + } + + public static function mkheader(int $l): string + { + $buf = chr(($l>>8)&0xff); + $buf .= chr($l&0xff); + + return $buf; + } +} \ No newline at end of file diff --git a/app/Classes/Protocol/EMSI.php b/app/Classes/Protocol/EMSI.php new file mode 100644 index 0000000..af24ee0 --- /dev/null +++ b/app/Classes/Protocol/EMSI.php @@ -0,0 +1,1177 @@ +self::P_HYDRA4, + //'8'=>self::P_HYDRA8, + //'6'=>self::P_HYDRA16, + //'H'=>self::P_HYDRA, + //'J'=>self::P_JANUS, + //'D'=>self::P_DIRZAP, + //'Z'=>self::P_ZEDZAP, + '1'=>self::P_ZMODEM, + ]; + + /** + * Incoming EMSI session + * + * @param SocketClient $client + * @return int|null + * @throws SocketException + * @throws Exception + */ + public function onConnect(SocketClient $client): ?int + { + // If our parent returns a PID, we've forked + if (! parent::onConnect($client)) { + $this->session(self::SESSION_AUTO,$client,(new Node)); + $this->client->close(); + Log::info(sprintf('%s: = End - Connection closed [%s]',__METHOD__,$client->getAddress())); + } + + return NULL; + } + + /** + * Send our welcome banner + * + * @throws Exception + */ + private function emsi_banner(): void + { + Log::debug(sprintf('%s: + Start',__METHOD__)); + + $banner = 'This is a mail only system - unless you are a mailer, you should disconnect :)'; + $this->client->buffer_add(self::EMSI_REQ.str_repeat(self::DEL,strlen(self::EMSI_REQ)).$banner.self::CR); + $this->client->buffer_flush(5); + } + + /** + * Create the EMSI_DAT + * + * @return string + */ + private function emsi_makedat(): string + { + $makedata = sprintf('%s0000',self::EMSI_DAT); + + /* + * Link Codes + * + * Link codes is a string of flags that specify desired connect conditions. These codes are separated by commas. + * New codes may be added with prior approval from the author of this document. + * + * Calling system options: + * PUA Pickup mail for all presented addresses. + * PUP Pickup mail for primary address only. + * NPU No mail pickup desired. + * + * Answering system options: + * HAT Hold ALL traffic. + * HXT Hold compressed mail traffic. + * HRQ Hold file requests (not processed at this time). + */ + $link_codes = $this->originate ? ['8N1','PUA'] : ['8N1']; + + /* + * Compatibility codes + * + * The calling system must list supported protocols first and descending order of preference (the most desirable + * protocol should be listed first). The answering system should only present one protocol and it should be the + * first item in the compatibility_codes field. + * + * Protocols + ----------------------------------------------------------------- + DZA* DirectZAP (Zmodem variant, reduced escape set). + TZA DirectZap (TrapDoor DirectZap varient) + ZAP ZedZap (Zmodem variant, upe 8K blocks). + ZMO** Zmodem w/1,024 packets (Wazoo ZedZip) + JAN Janus bi-directional. + KER Kermit. + HYD Hydra bi-directional (link flags define parameters) + SLK SeaLink (no TYSNC, No MDM7, No TeLink) + CHT Chat? + + Other codes + ----------------------------------------------------------------- + NCP No compatible protocols (failure). + NRQ No file requests accepted by this system. (IE: capability not implemented) + FRQ Node accepts and will process file rquests. + ARC ARCmail 0.60-capable, as defined by the FTSC. + XMA Supports other forms of compressed mail. + FNC Filename conversion. This indicates that any transmitted + files must follow the MS-DOS restrictions of an eight + character file name followed by a three character + extension; eg. FILENAME.EXT + DFB Indicates that the system presenting is capabable of fall-back to + FTS1/WAZOO negotiation in the case of failure of EMSI handshake or no + common protocol. + + Link Session options: + ----------------------------------------------------------------- + RMA Indicates that the presenting site is able to send and process multiple + file requests. If both sites present this flag, the caller will send + any REQ files found for each AKA presented by the answering system. + The answering system will process each received REQ. + PMO PickUp Mail (ARCmail and Packets) ONLY + NFE No TIC'S, associated files or files attachs desired + NXP No compressed mail pickup desired + NRQ File requests not accepted by caller + This flag is presented if file request processing + is disabled TEMPORARILY for any reason + */ + + // @todo We need to evaluate what the remote presents + $compat_codes = $this->originate ? ['ZMO','ARC','XMA'] : ['ZMO']; + + // Site address, password and compatibility + // @todo Only show the AKAs that is relevant to the node we are connecting to + $makedata .= sprintf('{EMSI}{%s}{%s}{%s}{%s}', + join(' ',$this->setup->nodes->pluck('ftn')->toArray()), + $this->node->password == '-' ? '' : $this->node->password, + join(',',$link_codes), + join(',',$compat_codes), + ); + + // Mailer Details + $makedata .= sprintf('{%s}{%s}{%s}{%s}', + self::product_code, + config('app.name'), + $this->setup->version, + '#000000' // Serial Numbers + ); + + // System Details + $makedata .= sprintf('{IDENT}{[%s][%s][%s][-Unpublished-][38400][%s]}', + $this->setup->system_name, + $this->setup->location, + $this->setup->sysop, + 'XA' // Nodelist Flags + ); + + $makedata .= sprintf('{TRAF}{%lX %lX}',$this->send->mail_size,$this->send->file_size); + + // @todo Not sure what MOH is for + //$makedata .= sprintf('{MOH#}{[%lX]}',0); + + $makedata .= sprintf('{TRX#}{[%lX]}',Carbon::now()->timestamp); + + $makedata .= sprintf('{TZUTC}{[%+03d%02d]}',10,0); + + // @todo Not sure what OHFR is for + //$makedata .= sprintf('{OHFR}{%s}','Never Never'); + + /* Calculate emsi length */ + $makedata = preg_replace('/0000/',sprintf('%04X',strlen($makedata)-14),$makedata,1); + + /* EMSI crc16 */ + $makedata .= sprintf('%04X',$this->CRC16USD(substr($makedata,2))); + + return $makedata; + } + + /** + * Parse the EMSI dat string and return chunks + * + * @param string $str + * @param int $x + * @param string $needle + * @return string + */ + private function emsi_dat_parse(string $str,int &$x,string $needle='}'): string + { + $y = $x; + $t = strpos($str,$needle,$x); + $x = $t+2; + + return substr($str,$y,$t-$y); + } + + /** + * Parse the received EMSI_DAT for remote system details + * + * @param string $str + * @return int + * @throws Exception + */ + private function emsi_parsedat(string $str): int + { + Log::debug(sprintf('%s: + Start',__METHOD__)); + + $l = 0; + + if (! ($str=strstr($str,self::EMSI_DAT))) { + Log::error(sprintf('%s: ! No EMSI_DAT signature found?',__METHOD__)); + + return 0; + } + + // Get our EMSI_DAT length + sscanf(substr($str,10),"%04X",$l); + + /* Bad EMSI length */ + if ($l != ($x=strlen($str)-18)) { + Log::error(sprintf('%s: ! Bad EMSI_DAT length: [%u], should be: [%u]!',__METHOD__,$l,$x)); + + return 0; + } + + // Check the CRC16 checksum + sscanf(substr($str,strlen($str)-4),"%04X",$l); + + /* Bad EMSI CRC */ + if ($l != ($x = $this->CRC16USD(substr($str,2,strlen($str)-6)))) { + Log::error(sprintf('%s: ! Bad EMSI_DAT CRC: [%04X], should be: [%04X]!',__METHOD__,$l,$x)); + + return 0; + } + + /* No EMSI ident */ + if (strncmp(substr($str,14),"{EMSI}",6)) { + Log::error(sprintf('%s: ! No EMSI fingerprint?',__METHOD__)); + + return 0; + } + + /* {AKAs} */ + $x = 21; + foreach (explode(' ',$this->emsi_dat_parse($str,$x)) as $rem_aka) { + Log::debug(sprintf('%s: - Parsing AKA [%s]',__METHOD__,$rem_aka)); + + try { + if (! ($o = Node::findFTN($rem_aka))) { + Log::debug(sprintf('%s: ? AKA is UNKNOWN [%s]',__METHOD__,$rem_aka)); + continue; + } + + } catch (Exception) { + Log::error(sprintf('%s: ! AKA is INVALID [%s]',__METHOD__,$rem_aka)); + + continue; + } + + // Check if the remote has our AKA + if ($this->setup->nodes->pluck('ftn')->search($o->ftn) !== FALSE) { + Log::error(sprintf('%s: ! AKA is OURS [%s]',__METHOD__,$o->ftn)); + + continue; + } + + // @todo lock nodes + Log::info(sprintf('%s: - Remote AKA [%s]',__METHOD__,$o->ftn)); + $this->node->ftn = $o; + } + + if ($this->originate AND ! $this->node->originate_check()) { + Log::error(sprintf('%s: ! We didnt get who we called?',__METHOD__)); + + return self::S_FAILURE|self::S_ADDTRY; + } + + // By definition, if we are in the DB, we are nodelisted + if ($this->node->aka_num) + $this->node->optionSet(self::O_LST); + + /* Password */ + $p = $this->emsi_dat_parse($str,$x); + if ($this->originate) { + $c = ($p === $this->node->password); + + } else { + $c = $this->node->auth($p); + } + + if (! $c) { + Log::info(sprintf('%s: - Remote has password [%s] on us',__METHOD__,$p)); + + if ($p) + $this->node->optionSet(self::O_BAD); + + } else { + $this->node->optionSet(self::O_PWD); + Log::info(sprintf('%s: - Remote Authed [%d] AKAs',__METHOD__,$c)); + $this->send->add('/tmp/aa'); + } + + /* Link codes */ + Log::notice(sprintf('%s: - Remote Link Codes [%s]',__METHOD__,$this->emsi_dat_parse($str,$x))); + + /* Compatibility codes */ + $codes = $this->emsi_dat_parse($str,$x); + if ($codes) + foreach (explode(',',$codes) as $code) { + switch ($code) { + case 'ARC': + Log::debug(sprintf('%s: = Node accepts ARC mail bundle (ARC)',__METHOD__)); + break; + + case 'NRQ': + Log::debug(sprintf('%s: = No file requests accepted by this system (NRQ)',__METHOD__)); + $this->node->optionSet(self::O_NRQ); + break; + + case 'XMA': + Log::debug(sprintf('%s: = Node supports other forms of compressed mail (XMA)',__METHOD__)); + break; + + case 'ZAP': + Log::debug(sprintf('%s: = Remote wants ZEDZAP',__METHOD__)); + $this->node->optionSet(self::P_ZEDZAP); + break; + + case 'ZMO': + Log::debug(sprintf('%s: = Remote wants ZMODEM',__METHOD__)); + $this->node->optionSet(self::P_ZMODEM); + break; + + default: + Log::info(sprintf('%s: = Ignoring unknown option: [%s] ',__METHOD__,$code)); + } + } + + /* Mailer code */ + Log::notice(sprintf('%s: - Remote Mailer Code [%s]',__METHOD__,$this->emsi_dat_parse($str,$x))); // hex + + /* Mailer name */ + Log::notice(sprintf('%s: - Remote Mailer [%s]',__METHOD__,$this->emsi_dat_parse($str,$x))); + + /* Mailer version */ + Log::notice(sprintf('%s: - Remote Mailer Version [%s]',__METHOD__,$this->emsi_dat_parse($str,$x))); + + /* Mailer serial number */ + Log::notice(sprintf('%s: - Remote Mailer Serial Number [%s]',__METHOD__,$this->emsi_dat_parse($str,$x))); + + while ($t=strpos($str,'}',$x)) { + $p = substr($str,$x,$t-$x); + $t++; // End of this field + + switch ($p) { + // {IDENT}{[]} + case 'IDENT': + /* System name */ + $x = $t+2; + Log::notice(sprintf('%s: - Remote System [%s]',__METHOD__,$this->emsi_dat_parse($str,$x,']'))); + + /* System location */ + Log::notice(sprintf('%s: - Remote Location [%s]',__METHOD__,$this->emsi_dat_parse($str,$x,']'))); + + /* Operator name */ + Log::notice(sprintf('%s: - Remote Operator [%s]',__METHOD__,$this->emsi_dat_parse($str,$x,']'))); + + /* Phone */ + Log::notice(sprintf('%s: - Remote Phone Number [%s]',__METHOD__,$this->emsi_dat_parse($str,$x,']'))); + + /* Baud rate */ + $this->client->setSpeed($this->emsi_dat_parse($str,$x,']')); + + /* Flags */ + Log::notice(sprintf('%s: - Remote Flags [%s]',__METHOD__,$this->emsi_dat_parse($str,$x,']'))); + $x++; + + break; + + // {TRAF}{} + case 'TRAF': + $x = $t+1; + Log::notice(sprintf('%s: - Remote TRAF [%s]',__METHOD__,$this->emsi_dat_parse($str,$x))); + + break; + + // {OHFR}{} + case 'OHFR': + $x = $t+1; + Log::notice(sprintf('%s: - Remote OHFR [%s]',__METHOD__,$this->emsi_dat_parse($str,$x))); + + break; + + // {MOH#}{[]} + case 'MOH#': + $x = $t+2; + Log::notice(sprintf('%s: - Remote MOH# [%s]',__METHOD__,$this->emsi_dat_parse($str,$x,']'))); + $x++; + + break; + + // {TRX#}{[]} + case 'TRX#': + $x = $t+2; + Log::notice(sprintf('%s: - Remote TRX [%s]',__METHOD__,$this->emsi_dat_parse($str,$x,']'))); + $x++; + + break; + + // {TZUTC}{[]} + case 'TZUTC': + $x = $t+2; + Log::notice(sprintf('%s: - Remote TZUTC [%s]',__METHOD__,$this->emsi_dat_parse($str,$x,']'))); + $x++; + + break; + + default: + $x = $t+1; + Log::notice(sprintf('%s: - Remote UNKNOWN [%s] (%s)',__METHOD__,$this->emsi_dat_parse($str,$x),$p)); + } + } + + return 1; + } + + /** + * STEP 2A, RECEIVE EMSI HANDSHAKE + * + * @throws SocketException + * @throws Exception + */ + private function emsi_recv(int $mode): int + { + Log::debug(sprintf('%s: + Start',__METHOD__)); + + Log::debug(sprintf('%s: - STEP 1',__METHOD__)); + /* + * Step 1 + * +-+------------------------------------------------------------------+ + * :1: Tries=0, T1=20 seconds, T2=60 seconds : + * +-+------------------------------------------------------------------+ + */ + + $p = ''; + $tries = 0; + $t1 = $this->client->timer_set(20); + $t2 = $this->client->timer_set(self::EMSI_HSTIMEOUT); + + do { + step2: + Log::debug(sprintf('%s: - STEP 2',__METHOD__)); + /* Step 2 + +-+------------------------------------------------------------------+ + :2: Increment Tries : + : : : + : : Tries>6? Terminate, and report failure. : + : +------------------------------------------------------------------+ + : : Are we answering system? Transmit EMSI_REQ, go to step 3. : + : +------------------------------------------------------------------+ + : : Tries>1? Transmit EMSI_NAK, go to step 3. : + : +------------------------------------------------------------------+ + : : Go to step 4. : + +-+------------------------------------------------------------------+ + */ + + if (++$tries > 6) + return self::TIMEOUT; + + if ($mode == self::SM_INBOUND) { + $this->client->buffer_add(self::EMSI_REQ.self::CR); + + } elseif ($tries > 1) { + $this->client->buffer_add(self::EMSI_NAK.self::CR); + + } else { + goto step4; + } + + $this->client->buffer_flush(5); + + step3: + Log::debug(sprintf('%s: - STEP 3',__METHOD__)); + /* Step 3 + * +-+------------------------------------------------------------------+ + * :3: T1=20 seconds : + * +-+------------------------------------------------------------------+ + */ + + $t1 = $this->client->timer_set(20); + + step4: + Log::debug(sprintf('%s: - STEP 4',__METHOD__)); + /* Step 4 + +-+------------------------------------------------------------------+ + :4: Wait for EMSI sequence until EMSI_HBT or EMSI_DAT or any of the : + : : timers have expired. : + : : : + : : If T2 has expired, terminate call and report failure. : + : +------------------------------------------------------------------+ + : : If T1 has expired, go to step 2. : + : +------------------------------------------------------------------+ + : : If EMSI_HBT received, go to step 3. : + : +------------------------------------------------------------------+ + : : If EMSI_DAT received, go to step 5. : + : +------------------------------------------------------------------+ + : : Go to step 4. : + +-+------------------------------------------------------------------+ + */ + + $got = 0; + + while (TRUE) { + $ch = $this->client->read_ch(max( 1,min($this->client->timer_rest($t1),$this->client->timer_rest($t2)))); + ///Log::debug(sprintf('%s: - Got [%x]{%d} (%c)',__METHOD__,$ch,$ch,$ch)); + + if (($ch != self::TIMEOUT) && ($ch < 0)) + return $ch; + + if ($this->client->timer_expired($t2)) + return self::TIMEOUT; + + /* goto step2; */ + if ($this->client->timer_expired($t1)) + break; + + if ($ch == self::TIMEOUT) + continue; + + if (! $got) { + if ($ch == ord('*')) + $got = 1; + else + continue; + } + + if (($ch == ord(self::CR)) || ($ch == ord(self::NL))) { + if (! strncmp($p,self::EMSI_HBT,self::EMSI_SEQ_LEN)) { + Log::debug(sprintf('%s: - Received EMSI_HBT',__METHOD__)); + + goto step3; + } + + if (! strncmp($p,self::EMSI_DAT,10)) { + Log::debug(sprintf('%s: - Received EMSI_DAT',__METHOD__)); + + Log::debug(sprintf('%s: - STEP 5',__METHOD__)); + /* Step 5 + +-+------------------------------------------------------------------+ + :5: Receive EMSI_DAT packet : + : +------------------------------------------------------------------+ + : : Packet received OK? Transmit EMSI_ACK twice, and : + : : go to step 6. : + : +------------------------------------------------------------------+ + : : Go to step 2. : + +-+------------------------------------------------------------------+ + */ + + $ch = $this->emsi_parsedat($p); + if ($ch) { + $this->client->buffer_add(self::EMSI_ACK.self::CR); + $this->client->buffer_add(self::EMSI_ACK.self::CR); + $this->client->buffer_flush(5); + + Log::debug(sprintf('%s: - STEP 6',__METHOD__)); + /* Step 6 + +-+------------------------------------------------------------------+ + :6: Received EMSI_DAT packet OK, exit. : + +-+------------------------------------------------------------------+ + */ + + return self::OK; + + } else { + Log::debug(sprintf('%s: - EMSI_DAT didnt parse',__METHOD__)); + + goto step2; + } + } + + $p = '';// Clear our EMSI dat since the return is the end of a transmission and its not what we want. + goto step4; + + } else { + if (strlen($p) >= self::EMSI_BUF) { + Log::warning(sprintf('%s: ! EMSI_DAT packet too long.',__METHOD__)); + + $rew = strstr($p,self::EMSI_BEG,TRUE); + if ($rew && $rew != $p) { + Log::notice(sprintf('%s: - Got EMSI_DAT at offset [%d].',__METHOD__,strlen($rew))); + + $p = substr($p,strlen($rew)); + } + } + + if ($ch > 31 && $ch <= 255) + $p .= chr($ch); + } + } + + } while(! $this->client->timer_expired($t2)); + + return self::TIMEOUT; + } + + /** + * STEP 2B, TRANSMIT EMSI HANDSHAKE + * + * @throws SocketException + * @throws Exception + */ + private function emsi_send(): int + { + Log::debug(sprintf('%s: + Start',__METHOD__)); + + Log::debug(sprintf('%s: - STEP 1',__METHOD__)); + /* Step 1 + +-+------------------------------------------------------------------+ + :1: Tries=0, T1=60 seconds : + +-+------------------------------------------------------------------+ + */ + + $p = ''; + $tries = 0; + $t1 = $this->client->timer_set(self::EMSI_HSTIMEOUT); + + step2: + Log::debug(sprintf('%s: - STEP 2',__METHOD__)); + /* Step 2 + +-+------------------------------------------------------------------+ + :2: Transmit EMSI_DAT packet and increment Tries : + : : : + : +------------------------------------------------------------------+ + : : Tries>6? Terminate, and report failure. : + : +------------------------------------------------------------------+ + : : Go to step 3. : + +-+------------------------------------------------------------------+ + */ + if (++$tries > 6) + return self::TIMEOUT; + + $this->client->buffer_add($this->emsi_makedat().self::CR); + $this->client->buffer_flush(5); + + /* Step 3 + +-+------------------------------------------------------------------+ + :3: T2=20 seconds : + +-+------------------------------------------------------------------+ + */ + Log::debug(sprintf('%s: - STEP 3',__METHOD__)); + $t2 = $this->client->timer_set(20); + + /* Step 4 + +-+------------------------------------------------------------------+ + :4: Wait for EMSI sequence until T1 has expired : + : : : + : : If T1 has expired, terminate call and report failure. : + : +------------------------------------------------------------------+ + : : If T2 has expired, go to step 2. : + : +------------------------------------------------------------------+ + : : If EMSI_REQ received, go to step 4. : + : +------------------------------------------------------------------+ + : : If EMSI_ACK received, go to step 5. : + : +------------------------------------------------------------------+ + : : If any other sequence received, go to step 2. : + +-+------------------------------------------------------------------+ + */ + Log::debug(sprintf('%s: - STEP 4',__METHOD__)); + while(! $this->client->timer_expired($t1)) { + $ch = $this->client->read_ch(max( 1,min($this->client->timer_rest($t1),$this->client->timer_rest($t2)))); + // Log::debug(sprintf('%s: - Got (%x) {%03d} (%c)',__METHOD__,$ch,$ch,$ch)); + + if (($ch != self::TIMEOUT) && ($ch < 0)) + return $ch; + + if ($this->client->timer_expired($t2)) + goto step2; + + if ($this->client->timer_expired($t1)) + return self::TIMEOUT; + + if ($ch == self::TIMEOUT) + continue; + + $ch &= 0x7f; + + if (($ch == ord(self::CR)) || ($ch == ord(self::NL))) { + if (! $p) + continue; + + if (! strncmp($p,self::EMSI_DAT,10)) { + Log::warning(sprintf('%s: - Got unexpected EMSI_DAT - Argus?',__METHOD__)); + $this->client->buffer_add(self::EMSI_ACK); + $this->client->buffer_add(self::EMSI_ACK); + $this->client->buffer_flush(1); + + $t2 = $this->client->timer_set($this->client->timer_rest($t2) >> 2); + + } else if (! strncmp($p,self::EMSI_REQ,self::EMSI_SEQ_LEN)) { + Log::notice(sprintf('%s: - Got EMSI_REQ - skipping...',__METHOD__)); + + } else if (! strncmp($p,self::EMSI_ACK,self::EMSI_SEQ_LEN)) { + Log::debug(sprintf('%s: - Got EMSI_ACK',__METHOD__)); + + Log::debug(sprintf('%s: - STEP 5',__METHOD__)); + /* Step 5 + +-+------------------------------------------------------------------+ + :5: Received EMSI_ACK, exit. : + +-+------------------------------------------------------------------+ + */ + + return self::OK; + } + + $p = ''; + continue; + } + + /* Put new symbol in buffer */ + if ($ch > 31) { + if (strlen($p) < self::TMP_LEN) { + $p .= chr($ch); + + } else { + Log::warning(sprintf('%s: ! EMSI packet too long',__METHOD__)); + } + } + } /* goto step4; */ + + return self::TIMEOUT; + } + + private function is_freq_available(): int + { + return self::FR_NOTAVAILABLE; // @todo + /* + if (! cfgs(self::CFG_EXTRP ) && ! cfgs(self::CFG_SRIFRP)) { + return self::FR_NOTHANDLED; + } + + return ((cfgs(self::CFG_EXTRP) || cfgs(self::CFG_SRIFRP)) && checktimegaps(cfgs(self::CFG_FREQTIME))); + */ + } + + /** + * STEP 1, EMSI INIT + * + * @throws SocketException + * @throws Exception + */ + protected function protocol_init(): int + { + if ($this->DEBUG) + Log::debug(sprintf('%s: + Start',__METHOD__)); + + $got = 0; + $tries = 0; + $p = ''; + + if ($this->originate) { + $gotreq = 0; + + // Send a character to get a response from the remote + $t1 = $this->client->timer_set(self::EMSI_HSTIMEOUT); + do { + $this->client->send(ord(self::CR),1); + + } while (! $this->client->hasData(1) && ! $this->client->timer_expired($t1)); + + if ($this->client->timer_expired($t1)) + return self::TIMEOUT; + + $t2 = $this->client->timer_set(self::EMSI_RESEND_TO); + while (TRUE) { + $ch = $this->client->read_ch(max( 1,min($this->client->timer_rest($t1),$this->client->timer_rest($t2)))); + if ($this->DEBUG) + Log::debug(sprintf('%s: - Got [%x] (%c)',__METHOD__,$ch,$ch)); + + if (($ch != self::TIMEOUT) && ($ch < 0)) + return $ch; + + if ($this->client->timer_expired($t1)) + return self::TIMEOUT; + + if ($this->client->timer_expired($t2)) { + if ($this->setup->do_prevent && $tries == 0) { + $this->setup->do_prevent = 0; + + $this->client->buffer_add(self::EMSI_INQ.self::CR); + $this->client->buffer_flush(5); + + } else { + if (++$tries > 10) + return self::TIMEOUT; + + Log::debug(sprintf('%s: - Sending EMSI_INQ (Try %d of 10)...',__METHOD__,$tries)); + $this->client->buffer_add(self::EMSI_INQ.self::CR); + } + + $t2 = $this->client->timer_set(self::EMSI_RESEND_TO); + continue; + } + + if ($ch == self::TIMEOUT) + continue; + + $ch &= 0x7f; + + if (($ch == ord(self::CR)) || ($ch == ord(self::NL))) { + if (strstr($p,self::EMSI_REQ)) { + Log::info(sprintf('%s: - Got EMSI_REQ',__METHOD__)); + if ($gotreq++) + return self::OK; + + $this->client->buffer_add(self::EMSI_INQ); + $this->client->buffer_flush(5); + + } elseif ($p && strstr($p,self::EMSI_BEG) && strstr($p,self::EMSI_ARGUS1)) { + Log::info(sprintf('%s: - Got Intro [%s]',__METHOD__,$p)); + } + + continue; + } + + if ($ch > 31) + $p .= chr($ch); + + if (strlen($p) >= self::EMSI_BUF) + return self::ERROR; + } + } + + $this->client->rx_purge(); + $this->client->buffer_clear(); + $this->emsi_banner(); + + $t1 = $this->client->timer_set(self::EMSI_HSTIMEOUT); + $t2 = $this->client->timer_set(self::EMSI_RESEND_TO); + + while (! $this->client->timer_expired($t1)) { + $ch = $this->client->read_ch(max( 1,min($this->client->timer_rest($t1),$this->client->timer_rest($t2)))); + if ($this->DEBUG) + Log::debug(sprintf('%s: - Got [%x] (%c)',__METHOD__,$ch,$ch)); + + if (($ch != self::TIMEOUT) && ($ch < 0)) + return $ch; + + if ($this->client->timer_expired($t1)) + return self::TIMEOUT; + + if (($ch == self::TIMEOUT) && $this->client->timer_expired($t2)) { + if (! $got) { + $this->emsi_banner(); + $t2 = $this->client->timer_set(self::EMSI_RESEND_TO); + + } else { + $t2 = $t1; + } + + continue; + } + + $ch &= 0x7f; + + if ((! $got) && ($ch == ord('*'))) + $got = 1; + + if ($got && (($ch == ord(self::CR)) || ($ch == ord(self::NL)))) { + $got = 0; + + if (strstr($p, self::EMSI_INQ)) { + Log::info(sprintf('%s: - Got EMSI_REQ',__METHOD__)); + + return self::OK; + } + + } else { + if ($got && ($ch > 31)) + $p .= chr($ch); + + if (strlen($p) >= self::EMSI_BUF) + return self::ERROR; + } + } + + return self::TIMEOUT; + } + + /** + * Setup our EMSI session + * + * @return int + * @throws Exception + */ + protected function protocol_session(): int + { + Log::debug(sprintf('%s: + Start',__METHOD__)); + + $was_req = 0; + $got_req = 0; + + // Outbound session + if ($this->originate) { + $this->optionSet(self::O_PUA); + //$emsi_lo |= ($this->is_freq_available() <= self::FR_NOTAVAILABLE ) ? self::O_NRQ : $emsi_lo; + + if ($this->emsi_send() < 0) + return (self::S_REDIAL|self::S_ADDTRY); + + $rc = $this->emsi_recv(self::SM_OUTBOUND); + + if ($rc < 0) + return (self::S_REDIAL|self::S_ADDTRY); + + Log::info(sprintf('%s: - Starting outbound EMSI session to [%s]',__METHOD__,$this->client->getAddress())); + + // @todo Lock Node AKAs + + // Inbound session + } else { + $rc = $this->emsi_recv(self::SM_INBOUND); + + if ($rc < 0) { + Log::error(sprintf('%s: ! Unable to establish EMSI session',__METHOD__)); + + return (self::S_REDIAL|self::S_ADDTRY); + } + + Log::info(sprintf('%s: - Starting inbound EMSI session from [%s]',__METHOD__,$this->client->getAddress())); + + // @todo Lock Node AKAs + + if ($this->node->aka_authed) { + $xproto = $this->is_freq_available(); + + if ($xproto == self::FR_NOTHANDLED || $xproto == self::FR_NOTAVAILABLE) + $this->node->optionSet(self::O_HRQ); + } + + foreach ($this->protocols as $p => $key) { + if ($this->node->optionGet($key)) { + Log::debug(sprintf('%s: - Remote supports [%s] (%x)',__METHOD__,$p,$key)); + $this->optionSet($key); + } + } + + // Disable chat + //$this->node->optionClear(self::MO_CHAT); + + if (! $this->protocols) + $this->optionSet(self::P_NCP); + + if ($this->emsi_send() < 0) + return (self::S_REDIAL|self::S_ADDTRY); + } + + Log::info(sprintf('%s: - We have %lu%s mail, %lu%s files',__METHOD__,$this->send->mail_size,'b',$this->send->file_size,'b')); + + $proto = $this->originate ? $this->node->optionGet(self::P_MASK) : $this->optionGet(self::P_MASK); + + switch ($proto) { + case self::P_NONE: + case self::P_NCP: + Log::error(sprintf('%s: ! No compatible protocols',__METHOD__)); + + return self::S_FAILURE; + + case self::P_ZMODEM: + $t = 'ZModem-1k'; + break; + + case self::P_ZEDZAP: + $t = 'ZedZap'; + break; + + case self::P_DIRZAP: + $t = 'DirZap'; + break; + + case self::P_HYDRA4: + $t = 'Hydra-4k'; + break; + + case self::P_HYDRA8: + $t = 'Hydra-8k'; + break; + + case self::P_HYDRA16: + $t = 'Hydra-16k'; + break; + + case self::P_HYDRA: + $t = 'Hydra'; + break; + + case self::P_JANUS: + $t = 'Janus'; + break; + + default: + Log::error(sprintf('%s: ? Unknown Protocol [%s]',__METHOD__,$proto)); + $t = 'Unknown'; + } + + $xproto = ($this->optionGet(self::O_RH1) && ($this->node->optionGet(self::O_RH1))); + $x = (substr($t,1,1) == 'H' && $xproto ) ? 'x' : ''; + + Log::info(sprintf('%s: = Using [%s]',__METHOD__,$t)); + + Log::debug(sprintf('%s: = Options: %s%s%s%s%s%s%s%s%s%s%s', + __METHOD__,$x,$t, + ($this->node->optionGet(self::O_LST)) ? '/LST' : '', + ($this->node->optionGet(self::O_PWD)) ? '/PWD' : '', + ($this->node->optionGet(self::O_HXT)) ? '/MO': '', + ($this->node->optionGet(self::O_HAT)) ? '/HAT' : '', + ($this->node->optionGet(self::O_HRQ)) ? '/HRQ' : '', + ($this->node->optionGet(self::O_NRQ)) ? '/NRQ' : '', + ($this->node->optionGet(self::O_FNC)) ? '/FNC' : '', + ($this->node->optionGet(self::O_BAD)) ? '/BAD' : '', + ($this->node->optionGet(self::MO_CHAT)) ? '/CHT' : '' + )); + + //chatinit($this->rnode->opt & self::MO_CHAT ? proto : -1 ); + + switch ($proto) { + case self::P_ZEDZAP: + case self::P_DIRZAP: + case self::P_ZMODEM: + $this->client->cps = 1; + + $xproto = ($proto&self::P_ZEDZAP) ? self::CZ_ZEDZAP : (($proto&self::P_DIRZAP) ? self::CZ_DIRZAP : self::CZ_ZEDZIP); + + if ($this->originate) { + $rc = $this->wazoosend($xproto); + + if (! $rc) + $rc = $this->wazoorecv($xproto); + + if ($got_req && ! $rc) + $rc = $this->wazoosend($xproto); + + } else { + $rc = $this->wazoorecv($xproto|0x0100); + + if ($rc) + return self::S_REDIAL; + + $rc = $this->wazoosend($xproto); + + if ($was_req) + $rc = $this->wazoorecv($xproto); + } + + break; + + case self::P_HYDRA: + case self::P_HYDRA4: + case self::P_HYDRA8: + case self::P_HYDRA16: + switch ($proto) { + case self::P_HYDRA: + $rc = 1; + break; + + case self::P_HYDRA4: + $rc = 2; + break; + + case self::P_HYDRA8: + $rc = 4; + break; + + case self::P_HYDRA16: + $rc = 8; + break; + + default: + $rc = 1; + } + + //$rc = hydra($this->originate,$rc,$xproto); + break; + + case self::P_JANUS: + //$rc = janus(); + break; + + default: + return self::S_OK; + } + + return $rc ? self::S_REDIAL : self::S_OK; + } + + /** + * Receive a file with a transfer protocol + * + * @param int $zap + * @return bool + */ + private function wazoorecv(int $zap): bool + { + Log::debug(sprintf('%s: + Start',__METHOD__)); + + $rc = (new Zmodem)->zmodem_receive($this->client,$zap,$this->recv,$this->setup->inbound); + + return ($rc == self::RCDO || $rc == self::ERROR); + } + + /** + * Possibly receive something from the remote + * + * @param int $zap + * @return bool + * @throws Exception + */ + private function wazoosend(int $zap): bool + { + Log::debug(sprintf('%s: + Start [%d]',__METHOD__,$zap)); + + $z = new Zmodem; + + if (! $z->zmodem_sendinit($this->client,$zap) && $this->send->total_count) + $z->zmodem_sendfile($this->send); + + return ($z->zmodem_senddone()<0); + } +} diff --git a/app/Classes/Protocol/Zmodem.php b/app/Classes/Protocol/Zmodem.php new file mode 100644 index 0000000..6dbf634 --- /dev/null +++ b/app/Classes/Protocol/Zmodem.php @@ -0,0 +1,2552 @@ +session(self::SESSION_ZMODEM,$client); + $this->client->close(); + Log::info(sprintf('%s: = End - Connection closed [%s]',__METHOD__,$client->getAddress())); + } + + return NULL; + } + + /** + * Initialise our session + */ + private function init(SocketClient $client,int $canzap): void + { + $this->client = $client; + + /* Set all options to requested state -- this may be alerted by other side in ZSINIT */ + $this->ls_Protocol = self::LSZ_OPTCRC32|self::LSZ_OPTSKIPGUARD; + + switch ($canzap&0xff) { + case 2: + $this->ls_Protocol |= self::LSZ_OPTDIRZAP; + /* FALL THROUGH */ + + case 1: + $this->ls_Protocol |= self::LSZ_OPTZEDZAP; + /* FALL THROUGH */ + + case 0: + break; + + default: + Log::error(sprintf('%s: ! Strange cancap [%d]',__METHOD__,$canzap)); + } + + /* Maximum block size -- by protocol, may be reduced by window size later */ + $this->ls_MaxBlockSize = ($this->ls_Protocol&self::LSZ_OPTZEDZAP) ? 8192 : 1024; + + /* Calculate timeouts */ + /* Timeout for header waiting, if no data sent -- 3*TransferTime or 10 seconds */ + $this->ls_HeaderTimeout = (self::LSZ_MAXHLEN * 30) / $this->client->speed; + $this->ls_HeaderTimeout = ($this->ls_HeaderTimeout > 10) ? $this->ls_HeaderTimeout : 10; + + /* Timeout for data packet (3*TransferTime or 60 seconds) */ + $this->ls_DataTimeout = ($this->ls_MaxBlockSize * 30) / $this->client->speed; + $this->ls_DataTimeout = ($this->ls_DataTimeout > 60) ? $this->ls_DataTimeout : 60; + + $this->ls_SkipGuard = ($this->ls_Protocol&self::LSZ_OPTSKIPGUARD) ? 1 : 0; + } + + public function protocol_init(): int + { + // Not used + } + + /** + * Setup our ZMODEM session + * + * @return int + * @throws \Exception + */ + public function protocol_session(): int + { + $proto = $this->originate ? $this->node->optionGet(self::P_MASK) : $this->optionGet(self::P_MASK); + + if ($this->originate) { + if (! $z->zmodem_sendinit($this->client,$proto) && $this->send->total_count) + $this->zmodem_sendfile($this->send); + + $rc = $this->zmodem_senddone(); + + } else { + $rc = $this->zmodem_receive($this->client,$proto,$this->recv,'.'); + } + + return $rc; + } + + /** + * Receive files via Zmodem + * + * @param SocketClient $client + * @param Receive $recv + * @param string $dir + * @param int $canzap + * @return int + */ + public function zmodem_receive(SocketClient $client,int $canzap,Receive $recv,string $dir): int + { + Log::debug(sprintf('%s: + Start [%d] into dir [%s]',__METHOD__,$canzap,$dir)); + + $opts = $this->init($client,$canzap); + + $this->ls_txWinSize = self::LSZ_WINDOW; + $this->recv = $recv; + + if ($canzap&0x0100) + $opts |= self::LSZ_OPTFIRSTBATCH; + + switch ($rc=$this->ls_zrecvfinfo(self::ZRINIT,($this->ls_Protocol&self::LSZ_OPTFIRSTBATCH) ? 1 : 0)) { + case self::ZFIN: + Log::debug(sprintf('%s: = ZFIN after INIT, empty batch',__METHOD__)); + $this->ls_zdonereceiver(); + + return self::LSZ_OK; + + case self::ZFILE: + Log::debug(sprintf('%s: = ZFILE after INIT',__METHOD__)); + break; + + default: + Log::error(sprintf('%s: ! Something strange after init [%d]',__METHOD__,$rc)); + + $this->ls_zabort(); + $this->ls_zdonereceiver(); + + return self::LSZ_ERROR; + } + + while (TRUE) { + switch ($rc) { + case self::ZFIN: + Log::debug(sprintf('%s: = ZFIN',__METHOD__)); + $this->ls_zdonereceiver(); + + return self::LSZ_OK; + + case self::ZFILE: + if (! $this->recv->to_get) { + Log::error(sprintf('%s: ! No files to get?',__METHOD__)); + + $frame = self::ZSKIP; + + } else { + switch ($this->recv->open()) { + case self::FOP_SKIP: + Log::info(sprintf('%s: = Skip this file [%s]',__METHOD__,$this->recv->name)); + $frame = self::ZSKIP; + + break; + + case self::FOP_SUSPEND: + Log::info(sprintf('%s: = Suspend this file [%s]',__METHOD__,$this->recv->name)); + $frame = self::ZFERR; + + break; + + case self::FOP_CONT: + case self::FOP_OK: + Log::info(sprintf('%s: = Receving [%s] from [%d]',__METHOD__,$this->recv->name,$this->recv->filepos)); + $frame = self::ZRINIT; + + switch (($rc=$this->ls_zrecvfile($recv->filepos))) { + case self::ZFERR: + Log::debug(sprintf('%s: = ZFERR',__METHOD__)); + $this->recv->close(); + $frame = self::ZFERR; + + break; + + case self::ZSKIP: + Log::debug(sprintf('%s: = ZSKIP',__METHOD__)); + $this->recv->close(); + $frame = self::ZSKIP; + + break; + + case self::LSZ_OK: + Log::debug(sprintf('%s: = OK',__METHOD__)); + $this->recv->close(); + + break; + + default: + Log::error(sprintf('%s: ! OTHER [%d]',__METHOD__,$rc)); + $this->recv->close(); + + return self::LSZ_ERROR; + } + + break; + } + } + + break; + + case self::ZABORT: + Log::debug(sprintf('%s: = ZABORT',__METHOD__)); + + $this->ls_zabort(); + $this->ls_zdonereceiver(); + + return self::LSZ_ERROR; + + default: + Log::error(sprintf('%s: ? Something strange [%d]',__METHOD__,$rc)); + + $this->ls_zabort(); + $this->ls_zdonereceiver(); + + return self::LSZ_ERROR; + } + + $rc = $this->ls_zrecvfinfo($frame,1); + } + + return self::LSZ_OK; + } + + /** + * Done sender -- good way + * + * @return int + * @throws \Exception + */ + public function zmodem_senddone():int + { + $trys = 0; + $retransmit = 1; + $this->txbuf = ''; + + do { + if ($retransmit) { + if (($rc=$this->ls_zsendhhdr(self::ZFIN,$this->ls_storelong(0))) < 0) + return $rc; + + $trys++; + $retransmit = 0; + } + + switch (($rc=$this->ls_zrecvhdr($this->ls_rxHdr,$this->ls_HeaderTimeout))) { + /* Ok, GOOD */ + case self::ZFIN: + $this->client->buffer_add('OO'); + $this->client->buffer_flush(5); + + return self::LSZ_OK; + + case self::ZNAK: + case self::LSZ_TIMEOUT: + $retransmit = 1; + + break; + + default: + Log::error(sprintf('%s: ? Something strange [%d]',__METHOD__,$rc)); + + if ($rc < 0) + return $rc; + + $retransmit = 1; + } + + } while ($trys < 10); + + Log::error(sprintf('%s: ? Something strange or timeeout [%d]',__METHOD__,$rc)); + return $rc; + } + + /** + * Init sending -- wrapper + * + * @param SocketClient $client + * @param int $canzap + * @return int + */ + public function zmodem_sendinit(SocketClient $client,int $canzap): int + { + Log::debug(sprintf('%s: + Start [%d]',__METHOD__,$canzap)); + + $this->init($client,$canzap); + + if (($rc=$this->ls_zinitsender(self::LSZ_WINDOW,'')) < 0) + return $rc; + + Log::debug(sprintf('%s: ZMODEM Link Options %d/%d, %s%s%s%s', + __METHOD__, + $this->ls_MaxBlockSize, + $this->ls_txWinSize, + ($this->ls_Protocol&self::LSZ_OPTCRC32) ? 'CRC32' : 'CRC16', + ($this->ls_rxCould&self::LSZ_RXCANDUPLEX) ? ',DUPLEX' : '', + ($this->ls_Protocol&self::LSZ_OPTVHDR) ? ',VHEADER' : '', + ($this->ls_Protocol&self::LSZ_OPTESCAPEALL) ? ',ESCALL' : '' + )); + + Log::debug(sprintf('%s: = End [%x]',__METHOD__,$rc)); + return $rc; + } + + /** + * Send a file with the Zmodem Protocol + * + * @param Send $send + * @return int + */ + public function zmodem_sendfile(Send $send): int + { + Log::debug(sprintf('%s: + Start',__METHOD__)); + + try { + $send->open(); + $rc = $this->ls_zsendfile($send,$this->ls_SerialNum++,$send->total_count,$send->total_size); + + switch ($rc) { + case self::LSZ_OK: + $send->close(TRUE); + + case self::ZSKIP: + case self::ZFERR: + $send->close(FALSE); + break; + + default: + $send->close(FALSE); + $this->ls_zabort(); + break; + } + + return $rc; + + } catch (\Exception $e) { + Log::error(sprintf('%s: ! Error [%s]',__METHOD__,$e->getMessage())); + } + + return self::OK; + } + + /** + * Fetch long integer (4 bytes) from buffer, as it must be stored in header + * + * @param array $buf + * @return int + */ + private function ls_fetchlong(array $buf): int + { + $l = $buf[self::LSZ_P3]; + $l<<=8; + $l |= $buf[self::LSZ_P2]; + $l<<=8; + $l |= $buf[self::LSZ_P1]; + $l<<=8; + $l |= $buf[self::LSZ_P0]; + + return $l; + } + + /** + * Return 7bit character, strip XON/XOFF if not DirZap, with timeout + * + * @param int $timeout + * @return int + * @throws SocketException + */ + private function ls_read7bit(int $timeout): int + { + $this->ls_CANCount = 0; + + do { + if (($c = $this->client->read_ch($timeout)) < 0) + return $c; + + } while ((! ($this->ls_Protocol&self::LSZ_OPTDIRZAP)) && (($c == self::XON) || ($c == self::XOFF))); + + if ($c == self::CAN) { + if (++$this->ls_CANCount == 5) + return self::LSZ_CAN; + + } else { + $this->ls_CANCount = 0; + } + + if (ord($c) == 0) + return self::LSZ_ERROR; + + return $c&0x7f; + } + + /** + * Read one character, check for five CANs + * + * @param int $timeout + * @return int + */ + private function ls_readcanned(int $timeout): int + { + if (($ch = $this->client->read_ch($timeout)) < 0) + return $ch; + + if ($ch == self::CAN) { + if (++$this->ls_CANCount == 5) + return self::LSZ_CAN; + + } else { + $this->ls_CANCount = 0; + } + + return $ch&0xff; + } + + /** + * Read character as two hex digit + * + * @param int $timeout + * @return int + */ + private function ls_readhex(int $timeout): int + { + static $c = 0; + + if (! $this->ls_GotHexNibble) { + if (($c = $this->ls_readhexnibble($timeout)) < 0) + return $c; + + $c <<= 4; + } + + if (($c2 = $this->ls_readhexnibble($timeout)) >= 0) { + $this->ls_GotHexNibble = 0; + + return ($c|$c2); + + } else { + $this->ls_GotHexNibble = 1; + + return $c2; + } + } + + /** + * Read one hex character + * + * @param int $timeout + * @return int + */ + private function ls_readhexnibble(int $timeout): int + { + if (($c = $this->ls_readcanned($timeout)) < 0) + return $c; + + if (chr($c) >= '0' && chr($c) <= '9') { + return $c-ord('0'); + + } elseif (chr($c) >= 'a' && chr($c) <= 'f') { + return $c-ord('a')+10; + + } else { + /* will be CRC error */ + return 0; + } + } + + /** + * Return 8bit character, strip + * + * @param int $timeout + * @return int + */ + private function ls_readzdle(int $timeout): int + { + $r = 0; + + /* There was no ZDLE in stream, try to read one */ + if (! $this->ls_GotZDLE) { + do { + if (($c = $this->ls_readcanned($timeout)) < 0) + return $c; + + /* Check for unescaped XON/XOFF */ + if (! ($this->ls_Protocol&self::LSZ_OPTDIRZAP)) { + switch ($c) { + case self::XON: + case self::XON|0x80: + case self::XOFF: + case self::XOFF|0x80: + $c = self::LSZ_XONXOFF; + } + } + + if ($c == self::ZDLE) + $this->ls_GotZDLE = 1; + + elseif ($c != self::LSZ_XONXOFF) + return $c&0xff; + + } while($c == self::LSZ_XONXOFF); + } + + /* We will be here only in case of DLE */ + /* We have data */ + if (($c = $this->ls_readcanned($timeout)) >= 0) { + $this->ls_GotZDLE = 0; + + switch ($c) { + case self::ZCRCE: + case self::ZCRCG: + case self::ZCRCW: + case self::ZCRCQ: + $r = ($c|0x100); + break; + } + + /* + *chat* + if ($r && isset($rnode) && $rnode->opt&self::MO_CHAT) { + do { + $rr = $this->ls_readcanned($timeout); + // @todo to implement + $this->z_devrecv_c($rr,0); + } while($rr); + + $this->z_devsend_c(0); + } + */ + + if ($r) + return $r; + + switch ($c) { + case self::ZRUB0: + return self::ZDEL; + + case self::ZRUB1: + return (self::ZDEL|0x80); + + default: + if (($c&0x60) != 0x40) + return self::LSZ_BADCRC; + + return ($c^0x40)&0xff; + } + } + + return $c; + } + + /** + * Send one char with escaping + * + * @param int $c + * @throws \Exception + */ + private function ls_sendchar(int $c): void + { + $c &= 0xff; + + /* We are Direct ZedZap -- escape only */ + if ($this->ls_Protocol&self::LSZ_OPTDIRZAP) { + $esc = (self::ZDLE == $c); + + /* We are normal ZModem (may be ZedZap) */ + } else { + /* Receiver want to escape ALL */ + if (($this->ls_Protocol&self::LSZ_OPTESCAPEALL) && (($c&0x60) == 0)) { + $esc = 1; + + } else { + switch ($c) { + case self::XON: + case (self::XON|0x80): + case self::XOFF: + case (self::XOFF|0x80): + case self::DLE: + case (self::DLE|0x80): + case self::ZDLE: + $esc = 1; + break; + + default: + $esc = ((($this->ls_txLastSent&0x7f) == ord('@')) && (($c&0x7f) == self::CR)); + break; + } + } + } + + if ($esc) { + $this->client->buffer_add(chr(self::ZDLE)); + $c ^= 0x40; + } + + $this->client->buffer_add(chr($this->ls_txLastSent = $c)); + } + + /** + * Send one char as two hex digits + * + * @param int $i + * @throws \Exception + */ + private function ls_sendhex(int $c): void + { + $hexdigitslower = "0123456789abcdef"; + + $this->client->buffer_add(substr($hexdigitslower,($c&0xf0)>>4,1)); + $this->client->buffer_add(chr($this->ls_txLastSent = ord(substr($hexdigitslower,($c&0x0f),1)))); + } + + /** + * Store long integer (4 bytes) in buffer, as it must be stored in header + * + * @param int $l + * @return array + */ + private function ls_storelong(int $l): array + { + $buf[self::LSZ_P0] = ($l)&0xff; + $buf[self::LSZ_P1] = ($l>>8)&0xff; + $buf[self::LSZ_P2] = ($l>>16)&0xff; + $buf[self::LSZ_P3] = ($l>>24)&0xff; + + return $buf; + } + + /** + * Abort the session + * + * @throws \Exception + */ + private function ls_zabort(): void + { + $this->client->buffer_flush($this->ls_DataTimeout); + $this->client->buffer_add(chr(self::XON)); + + for ($i=0;$i<8;$i++) + $this->client->buffer_add(chr(self::CAN)); + + $this->client->buffer_flush($this->ls_DataTimeout); + } + + /** + * Finished receiving + * + * @return int + * @throws SocketException + */ + private function ls_zdonereceiver(): int + { + Log::debug(sprintf('%s: + Start',__METHOD__)); + + $trys = 0; + $retransmit = 1; + $this->rxbuf = ''; + + do { + if ($retransmit) { + if (($rc=$this->ls_zsendhhdr(self::ZFIN,$this->ls_storelong(0))) < 0) + return $rc; + + $retransmit = 0; + $trys++; + } + + switch ($rc=$this->client->read_ch($this->ls_HeaderTimeout)) { + /* Ok, GOOD */ + case ord('O'): + $rc = $this->client->read_ch(0); + return self::LSZ_OK; + + case self::XON: + case self::XOFF: + case self::XON|0x80: + case self::XOFF|0x80: + if ($this->DEBUG) + Log::debug(sprintf('%s: - XON/XOFF, skip it',__METHOD__)); + break; + + case self::ZPAD: + if ($this->DEBUG) + Log::debug(sprintf('%s: - ZPAD',__METHOD__)); + + if (($rc=$this->ls_zrecvhdr($this->ls_rxHdr,$this->ls_HeaderTimeout)) < 0) + return $rc; + + if (self::ZFIN != $rc) + return self::LSZ_OK; + + $retransmit = 1; + + break; + + default: + Log::error(sprintf('%s: ? Something strange [%d]',__METHOD__,$rc)); + + if ($rc < 0) + return $rc; + + $retransmit = 1; + } + + } while ($trys < 10); + + Log::error(sprintf('%s: ? Something strange [%d]',__METHOD__,$rc)); + return $rc; + } + + /** + * Init sender, preapre to send files (initialize timeouts too!) + * + * @param int $protocol + * @param int $window + * @return int + */ + private function ls_zinitsender(int $window,string $attstr) + { + Log::debug(sprintf('%s: + Start',__METHOD__)); + + $trys = 0; + + /* Options from ZRINIT header */ + $this->rxOptions = 0; + + /* ZFIN counter -- we will trust only MAY of them on this stage */ + $zfins = 0; + + $this->ls_SerialNum = 1; + + /* Why we need to send this? Old, good times... */ + $this->client->send("rz\r",5); + $retransmit = 1; + + do { + if ($retransmit) { + /* Send first ZRQINIT (do we need it?) */ + if (($rc=$this->ls_zsendhhdr(self::ZRQINIT,$this->ls_storelong(0))) < 0) + return $rc; + + $retransmit = 0; + $trys++; + } + + switch ($rc=$this->ls_zrecvhdr($this->ls_rxHdr,$this->ls_HeaderTimeout)) { + /* Ok, We got RINIT! */ + case self::ZRINIT: + Log::debug(sprintf('%s: - ZRINIT',__METHOD__)); + + $this->rxOptions= (($this->ls_rxHdr[self::LSZ_F1]&0xff)<<8)|($this->ls_rxHdr[self::LSZ_F0]&0xff); + + /* What receiver could (in hardware) -- Duplex, sim. I/O, send break signals */ + $this->ls_rxCould = ($this->rxOptions&(self::LSZ_RXCANDUPLEX|self::LSZ_RXCANOVIO|self::LSZ_RXCANBRK)); + + /* Strip RLE from ls_Protocol, if peer could not use RLE (WE COULD NOT RLE in ANY CASE!) */ + if (! ($this->rxOptions&self::LSZ_RXCANRLE)) + $this->ls_Protocol &= (~self::LSZ_OPTRLE); + + /* Strip CRC32 from $this->ls_Protocol, if peer could not use CRC32 */ + if (! ($this->rxOptions&self::LSZ_RXCANFC32)) + $this->ls_Protocol &= (~self::LSZ_OPTCRC32); + + /* Set EscapeAll if peer want it */ + if ($this->rxOptions&self::LSZ_RXWNTESCCTL) + $this->ls_Protocol |= self::LSZ_OPTESCAPEALL; + + /* Strip VHeaders from $this->ls_Protocol, if peer could not use VHDR */ + if (! ($this->rxOptions&self::LSZ_RXCANVHDR)) + $this->ls_Protocol &= (~self::LSZ_OPTVHDR); + + /* Ok, options are ready */ + /* Fetch window size */ + $this->ls_txWinSize = (($this->ls_rxHdr[self::LSZ_P1]&0xff)<<8)|($this->ls_rxHdr[self::LSZ_P0]&0xff); + + /* Override empty or big window by our window (if not emty too) */ + if ($window && (! $this->ls_txWinSize || ($this->ls_txWinSize > $window))) + $this->ls_txWinSize = $window; + + Log::debug(sprintf('%s: - ZRINIT OK - effproto [%08x], winsize [%d]',__METHOD__,$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) { + for ($this->ls_MaxBlockSize=1;$this->ls_MaxBlockSize<$this->ls_txWinSize;$this->ls_MaxBlockSize<<=1) {}; + + /*ls_MaxBlockSize >>= 1;*/ + if ($this->ls_MaxBlockSize<32) + $this->ls_txWinSize = $this->ls_MaxBlockSize=32; + } + + if ($this->client->speed < 2400) + $this->ls_txCurBlockSize = 256; + + elseif ($this->client->speed >= 2400 && $this->client->speed < 4800) + $this->ls_txCurBlockSize = 512; + + else + $this->ls_txCurBlockSize = 1024; + + if ($this->ls_Protocol&self::LSZ_OPTZEDZAP) { + if ($this->client->speed >= 7200 && $this->client->speed < 9600) + $this->ls_txCurBlockSize = 2048; + + elseif ($this->client->speed >= 9600 && $this->client->speed < 14400) + $this->ls_txCurBlockSize = 4096; + + elseif ($this->client->speed >= 14400) + $this->ls_txCurBlockSize = 8192; + } + + if ($this->ls_txCurBlockSize>$this->ls_MaxBlockSize) + $this->ls_txCurBlockSize=$this->ls_MaxBlockSize; + + Log::debug(sprintf('%s: - ZRINIT OK - block sizes Max [%d] Current [%d]',__METHOD__,$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))) + return $this->ls_zsendsinit($attstr); + + else + return self::LSZ_OK; + + /* Return number to peer, he is paranoid */ + case self::ZCHALLENGE: + Log::debug(sprintf('%s: - CHALLENGE',__METHOD__)); + + if (($rc=$this->ls_zsendhhdr(ZACK,$this->ls_rxHdr)) < 0) + return $rc; + + break; + + /* Send ZRQINIT again */ + case self::ZNAK: + case self::LSZ_TIMEOUT: + $retransmit = 1; + + break; + + /* ZFIN from previous session? Or may be real one? */ + case self::ZFIN: + Log::debug(sprintf('%s: - ZFIN [%d]',__METHOD__,$zfins)); + + if (++$zfins == self::LSZ_TRUSTZFINS) + return self::LSZ_ERROR; + + break; + + /* Please, resend */ + case self::LSZ_BADCRC: + Log::debug(sprintf('%s: - LSZ_BADCRC',__METHOD__)); + + /* We don't support it! */ + case self::ZCOMMAND: + Log::debug(sprintf('%s: - ZCOMMAND',__METHOD__)); + $this->ls_zsendhhdr(ZNAK,$this->ls_storelong(0)); + + /* Abort this session -- we trust in ABORT! */ + case self::ZABORT: + Log::debug(sprintf('%s: - ZABORT',__METHOD__)); + return self::LSZ_ERROR; + + default: + Log::error(sprintf('%s: ? Something strange [%d]',__METHOD__,$rc)); + + if ($rc < 0) + return $rc; + } + + } while ($trys < 10); + + Log::error(sprintf('%s: ? Something strange or timeout [%d]',__METHOD__,$rc)); + return $rc; + } + + private function ls_zrecvdata(&$data,&$len,$timeout,$crc32) { + return $crc32 ? $this->ls_zrecvdata32($data,$len,$timeout) : $this->ls_zrecvdata16($data,$len,$timeout); + } + + /** + * Receive data subframe with CRC32, return frame type or error (may be -- timeout) + * + * @param string $data + * @param int $len + * @param int $timeout + * @return int + */ + private function ls_zrecvdata32(string &$data,int &$len,int $timeout): int + { + if ($this->DEBUG) + Log::debug('+ Start',['m'=>__METHOD__,'d'=>$data]); + + $got = 0; /* Bytes total got */ + $incrc = self::LSZ_INIT_CRC32; /* Calculated CRC */ + $crc = 0; /* Received CRC */ + $frametype = self::LSZ_ERROR; /* Type of frame - ZCRC(G|W|Q|E) */ + $rcvdata = 1; /* Data is being received NOW (not CRC) */ + + while ($rcvdata && (($c=$this->ls_readzdle($timeout)) >= 0)) { + if ($this->DEBUG) + Log::debug(sprintf(' - got [%x] (%c)',$c,($c<31 ? 32 : $c)),['m'=>__METHOD__,'c'=>serialize($c)]); + + if ($c < 256) { + $data .= chr($c&0xff); + + if (++$got > $this->ls_MaxBlockSize) + return self::LSZ_BADCRC; + + $incrc = $this->CRC32_UPDATE($c,$incrc); + + } else { + switch ($c) { + case self::LSZ_CRCE: + case self::LSZ_CRCG: + case self::LSZ_CRCQ: + case self::LSZ_CRCW: + $rcvdata = 0; + $frametype = ($c&0xff); + $incrc = $this->CRC32_UPDATE($c,$incrc); + + break; + + default: + return self::LSZ_BADCRC; + } + } + } + + /* We finish loop by error in ls_readzdle() */ + if ($rcvdata) { + Log::error(sprintf('%s: ? Something strange or timeout [%d]',__METHOD__,$rc)); + + return $c; + } + + /* Loops ar unrolled */ + if (($c = $this->ls_readzdle($timeout)) < 0) + return $c; + + $crc |= ($c << 0x00); + if (($c = $this->ls_readzdle($timeout)) < 0) + return $c; + + $crc |= ($c << 0x08); + if (($c = $this->ls_readzdle($timeout)) < 0) + return $c; + + $crc |= ($c << 0x10); + if (($c = $this->ls_readzdle($timeout)) < 0) + return $c; + + $crc |= ($c << 0x18); + $incrc = $this->CRC32_FINISH($incrc); + + if ($this->DEBUG) + Log::debug(sprintf('CRC%d got %08x - calculated %08x',32,$incrc,$crc),['m'=>__METHOD__,'crc'=>$crc,'test_crc32'=>sprintf('%08x',$this->CRC32($data))]); + + if ($incrc != $crc) + return self::LSZ_BADCRC; + + $len = $got; + + if ($this->DEBUG) + Log::debug('= End',['m'=>__METHOD__,'frametype'=>$frametype]); + + return $frametype; + } + + /** + * Receive one file + * + * @param int $pos + * @return int + * @throws \Exception + */ + private function ls_zrecvfile(int $pos): int + { + Log::debug('+ Start',['m'=>__METHOD__,'pos'=>$pos]); + + $needzdata = 1; + $len = 0; + $rxpos = $pos; + $rxstatus = 0; + $this->rxbuf = ''; + + if (($rc=$this->ls_zsendhhdr(self::ZRPOS,$this->ls_storelong($rxpos))) < 0) + return $rc; + + do { + if (! $needzdata) { + switch (($rc=$this->ls_zrecvdata($this->rxbuf,$len,$this->ls_DataTimeout,$this->ls_Protocol&self::LSZ_OPTCRC32))) { + case self::ZCRCE: + $needzdata = 1; + + case self::ZCRCG: + Log::debug(sprintf('%s: - ZCRC%s, [%d] bytes at [%d]',__METHOD__,($rc==self::ZCRCE ? 'E' : 'G'),$len,$rxpos)); + + $rxpos += $len; + + if ($len != $this->recv->write($this->rxbuf)) + return self::ZFERR; + + $this->rxbuf = ''; + + break; + + case self::ZCRCW: + $needzdata = 1; + + case self::ZCRCQ: + Log::debug(sprintf('%s: - ZCRC%s, [%d] bytes at [%d]',__METHOD__,($rc==self::ZCRCW ? 'W' : 'Q'),$len,$rxpos)); + + $rxpos += $len; + + if ($len != $this->recv->write($this->rxbuf)) + return self::ZFERR; + + $this->rxbuf = ''; + + $this->ls_zsendhhdr(self::ZACK,$this->ls_storelong($rxpos)); + + break; + + case self::LSZ_BADCRC: + case self::LSZ_TIMEOUT: + if ($this->ls_rxAttnStr) { + $this->client->buffer_add($this->ls_rxAttnStr); + $this->client->buffer_flush(5); + } + + $this->client->rx_purge(); + $this->ls_zsendhhdr(self::ZRPOS,$this->ls_storelong($rxpos)); + $needzdata = 1; + + break; + + default: + Log::error(sprintf('%s: ? Something strange [%d]',__METHOD__,$rc)); + + if ($rc < 0) + return $rc; + + if ($this->ls_rxAttnStr) { + $this->client->buffer_add($this->ls_rxAttnStr); + $this->client->buffer_flush(5); + } + + $this->client->rx_purge(); + $this->ls_zsendhhdr(self::ZRPOS,$this->ls_storelong($rxpos)); + $needzdata = 1; + } + + /* We need new position -- ZDATA (and may be ZEOF) */ + } else { + Log::debug(sprintf('%s: - Want ZDATA/ZEOF at [%d]',__METHOD__,$rxpos)); + + if (($rc=$this->ls_zrecvnewpos($rxpos,$newpos)) < 0) + return $rc; + + if ($newpos != $rxpos) { + Log::error(sprintf('%s: - Bad new position [%d] in [%d]',__METHOD__,$newpos,$rc)); + + if ($this->ls_rxAttnStr) { + $this->client->buffer_add($this->ls_rxAttnStr); + $this->client->buffer_flush(5); + } + + $this->client->rx_purge(); + + if (($rc=$this->ls_zsendhhdr(self::ZRPOS,$this->ls_storelong($rxpos))) < 0) + return $rc; + + } else { + if ($rc == self::ZEOF) { + Log::debug(sprintf('%s: - ZEOF',__METHOD__)); + + if (($rc=$this->ls_zsendhhdr(self::ZRINIT,$this->ls_storelong(0))) < 0) + return $rc; + + return self::LSZ_OK; + } + + Log::debug(sprintf('%s: - ZDATA',__METHOD__)); + $needzdata = 0; + } + } + + if ($rxstatus) + return ($rxstatus==self::RX_SKIP) ? self::ZSKIP : self::ZFERR; + + } while(TRUE); + + return self::LSZ_OK; + } + + /** + * @param int $frame + * @param int $first + * @return int + * @throws \Exception + */ + private function ls_zrecvfinfo(int $frame,int $first): int + { + Log::debug(sprintf('%s: + Start - Frame [%d], First [%d]',__METHOD__,$frame,$first)); + + $trys = 0; + $retransmit = ($frame != self::ZRINIT); + $len = 0; + $rc = 0; + + /* ZFIN counter -- we will trust only MAY of them on first stage */ + $zfins = 0; + + $win = $this->ls_txWinSize; + $flags = (self::LSZ_RXCANDUPLEX|self::LSZ_RXCANOVIO); + + if ($this->ls_Protocol&self::LSZ_OPTCRC32) + $flags |= self::LSZ_RXCANFC32; + + if ($this->ls_Protocol&self::LSZ_OPTESCAPEALL) + $flags |= self::LSZ_RXWNTESCCTL; + + if ($this->ls_Protocol&self::LSZ_OPTESC8) + $flags |= self::LSZ_RXWNTESC8; + + do { + if ($retransmit) { + if ($frame != self::ZRINIT) { + if (($rc=$this->ls_zsendhhdr($frame,$this->ls_storelong($this->ls_SerialNum))) < 0) + return $rc; + } + + Log::debug(sprintf('%s: - ZRINIT',__METHOD__)); + + $txHdr = []; + $txHdr[self::LSZ_P0] = ($win&0xff); + $txHdr[self::LSZ_P1] = (($win>>8)&0xff); + $txHdr[self::LSZ_F1] = ($this->ls_Protocol&self::LSZ_OPTVHDR) ? self::LSZ_RXCANVHDR : 0; + $txHdr[self::LSZ_F0] = $flags; + + if (($rc=$this->ls_zsendhhdr(self::ZRINIT,$txHdr)) < 0) + return $rc; + + $retransmit = 0; + $trys++; + } + + switch (($rc=$this->ls_zrecvhdr($this->ls_rxHdr,$this->ls_HeaderTimeout))) { + /* Send ZRINIT again */ + case self::ZRQINIT: + Log::debug(sprintf('%s: - ZRQINIT',__METHOD__)); + /* We will trust in first ZFIN after ZRQINIT */ + $first = 1; + + case self::ZNAK: + Log::debug(sprintf('%s: - ZNAK',__METHOD__)); + + case self::LSZ_TIMEOUT: + Log::debug(sprintf('%s: - LSZ_TIMEOUT',__METHOD__)); + $retransmit = 1; + + break; + + /* He want to set some options */ + case self::ZSINIT: + Log::debug(sprintf('%s: - ZSINIT',__METHOD__)); + if (($rc=$this->ls_zrecvcrcw($this->rxbuf,$len)) < 0) + return $rc; + + /* We will trust in first ZFIN after ZSINIT */ + $first = 0; + + /* Everything is OK */ + if (! $rc) { + $this->ls_zsendhhdr(self::ZACK,$this->ls_storelong(1)); + + $this->ls_rxAttnStr = $this->rxbuf; + + if ($this->ls_rxHdr[self::LSZ_F0]&self::LSZ_TXWNTESCCTL) + $this->ls_Protocol |= self::LSZ_OPTESCAPEALL; + + if ($this->ls_rxHdr[self::LSZ_F0]&self::LSZ_TXWNTESC8) + $this->ls_Protocol |= self::LSZ_OPTESC8; + + /* We could not receive ZCRCW subframe, but error is not fatal */ + } else { + $trys++; + } + + break; + + /* Ok, File started! */ + case self::ZFILE: + Log::debug(sprintf('%s: - ZFILE',__METHOD__)); + + if (($rc=$this->ls_zrecvcrcw($this->rxbuf,$len)) < 0) + return $rc; + + /* Everything is OK, decode frame */ + if (! $rc) { + $file = []; + $file['name'] = substr($this->rxbuf,0,$x=strpos($this->rxbuf,chr(0x00))); + + if (sscanf(substr($this->rxbuf,$x+1), + '%ld %lo %o %o %ld %ld', + $file['size'], + $file['mtime'], + $len, // @todo What is $len? + $ls_SerialNum, // @todo Do we use this? + $filesleft, // @todo Should track this + $bytesleft) < 2) + { + Log::error(sprintf('%s: ! File info is corrupted [%s]',__METHOD__,$this->rxbuf)); + $filesleft = -1; + + } else { + $this->recv->new($file); + } + + return self::ZFILE; + + /* We could not receive ZCRCW subframe, but error is not fatal */ + } else { + $trys++; + } + + break; + + /* ZFIN from previous session? Or may be real one? */ + case self::ZFIN: + Log::debug(sprintf('%s: - ZFIN [%d], first [%d]',__METHOD__,$zfins,$first)); + + if ($first || (++$zfins == self::LSZ_TRUSTZFINS)) + return self::ZFIN; + + break; + + /* Abort this session -- we trust in ABORT! */ + case self::ZABORT: + Log::debug(sprintf('%s: - ZABORT',__METHOD__)); + return self::ZABORT; + + case self::LSZ_BADCRC: + Log::debug(sprintf('%s: - BADCRC',__METHOD__)); + + $this->ls_zsendhhdr(self::ZNAK,$this->ls_storelong(0)); + $retransmit = 1; + + break; + + default: + Log::error(sprintf('%s: ? Something strange [%d]',__METHOD__,$rc)); + + if ($rc < 0) + return $rc; + + $this->ls_zsendhhdr(self::ZNAK,$this->ls_storelong(0)); + } + + } while ($trys < 10); + + Log::error(sprintf('%s: ? Something strange or timeout',__METHOD__)); + return self::LSZ_TIMEOUT; + } + + private function ls_zrecvhdr(array &$hdr,int $timeout): int + { + if ($this->DEBUG) + Log::debug(sprintf('%s: + Start with [%d] timeout',__METHOD__,$timeout)); + + $state = self::rhInit; + $readmode = self::rm7BIT; + + static $frametype = self::LSZ_ERROR; /* Frame type */ + static $crcl = 2; /* Length of CRC (CRC16 is default) */ + static $crcgot = 0; /* Number of CRC bytes already got */ + static $incrc = 0; /* Calculated CRC */ + static $crc = 0; /* Received CRC */ + static $len = 4; /* Length of header (4 is default) */ + static $got = 0; /* Number of header bytes already got */ + static $garbage = 0; /* Count of garbage characters */ + $c = -1; + + if ($state == self::rhInit) { + if ($this->DEBUG) + Log::debug(sprintf('%s: - Init State',__METHOD__)); + + $frametype = self::LSZ_ERROR; + $crc = 0; + $crcl = 2; + $crcgot = 0; + $incrc = self::LSZ_INIT_CRC16; + $len = 4; + $got = 0; + $readmode = self::rm7BIT; + } + + while ($rc = $this->client->hasData($timeout)) { + if ($this->DEBUG) + Log::debug(sprintf('%s: - hasData - readmode [%d] - Garbage [%d]',__METHOD__,$readmode,$garbage)); + + switch ($readmode) { + case self::rm8BIT: + $c = $this->ls_readcanned($timeout); + break; + case self::rm7BIT: + $c = $this->ls_read7bit($timeout); + break; + case self::rmZDLE: + $c = $this->ls_readzdle($timeout); + break; + case self::rmHEX: + $c = $this->ls_readhex($timeout); + break; + } + + if ($this->DEBUG) + Log::debug(sprintf('%s: - %s [%x] (%c)',__METHOD__,$readmode,$c,$c)); + + /* Here is error */ + if ($c < 0) + return $c; + + /* Strip high bits */ + $c &= 0xff; + + if ($this->DEBUG) + Log::debug(sprintf('%s: = %x',__METHOD__,$c),['state'=>$state]); + + switch ($state) { + case self::rhInit: + if ($c == self::ZPAD) + $state = self::rhZPAD; + else + $garbage++; + + break; + + case self::rhZPAD: + switch ($c) { + case self::ZPAD: + break; + + case self::ZDLE: + $state = self::rhZDLE; + break; + + default: + $garbage++; + $state = self::rhInit; + break; + } + + break; + + case self::rhZDLE: + switch ($c) { + case self::ZBIN: + $state = self::rhZBIN; + $readmode = self::rmZDLE; + break; + + case self::ZHEX: + $state = self::rhZHEX; + $readmode = self::rmHEX; + break; + + case self::ZBIN32: + $state = self::rhZBIN32; + $readmode = self::rmZDLE; + break; + + case self::ZVBIN: + $state = self::rhZVBIN; + $readmode = self::rmZDLE; + break; + + case self::ZVHEX: + $state = self::rhZVHEX; + $readmode = self::rmHEX; + break; + + case self::ZVBIN32: + $state = self::rhZVBIN32; + $readmode = self::rmZDLE; + break; + + default: + $garbage++; + $state = self::rhInit; + $readmode = self::rm7BIT; + } + + break; + + case self::rhZVBIN32: + $crcl = 4; + /* Fall throught */ + + case self::rhZVBIN: + case self::rhZVHEX: + if ($c > self::LSZ_MAXHLEN) { + $state = self::rhInit; + + return self::LSZ_BADCRC; + } + + $len = $c; + $state = self::rhFrameType; + + break; + + case self::rhZBIN32: + $crcl = 4; + /* Fall throught */ + + case self::rhZBIN: + case self::rhZHEX: + $len = 4; + + case self::rhFrameType: + if (($c < 0) || ($c > self::LSZ_MAXFRAME)) { + $state = self::rhInit; + + return self::LSZ_BADCRC; + } + + $frametype = $c; + $incrc = ($crcl == 2) ? $this->CRC16USD_UPDATE($c,self::LSZ_INIT_CRC16) : $this->CRC32_UPDATE($c,self::LSZ_INIT_CRC32); + $state = self::rhBYTE; + + break; + + case self::rhBYTE: + if ($this->DEBUG) + Log::debug(sprintf('%s: - [%02x] (%d)',__METHOD__,$c,$got)); + + $hdr[$got] = $c; + + if ($len == ++$got) + $state = self::rhCRC; + + $incrc = ($crcl == 2) ? $this->CRC16USD_UPDATE($c,$incrc) : $this->CRC32_UPDATE($c,$incrc); + + break; + + case self::rhCRC: + if ($this->DEBUG) + Log::debug(sprintf('%s: - [%02x] (%d|%d)',__METHOD__,$c,$crcgot+1,$got)); + + if ($crcl == 2) { + $crc <<= 8; + $crc |= $c; + + } else { + $crc |= ($c << ($crcgot*8)); + } + + /* Crc finished */ + if ($crcl == ++$crcgot) { + $state = self::rhInit; + $garbage = 0; + + if ($crcl == 2) { + if (($this->ls_Protocol&self::LSZ_OPTCRC32) && ($readmode != self::rmHEX)) + Log::error(sprintf('%s: - was CRC32, got CRC16 binary header',__METHOD__)); + + $crc &= 0xffff; + + if ($readmode != self::rmHEX) + $this->ls_Protocol &= (~self::LSZ_OPTCRC32); + + } else { + if (! ($this->ls_Protocol&self::LSZ_OPTCRC32)) + Log::error(sprintf('s: - was CRC16, got CRC32 binary header',__METHOD__)); + + $incrc = $this->CRC32_FINISH($incrc); + $this->ls_Protocol |= self::LSZ_OPTCRC32; + } + + if ($this->DEBUG) + Log::debug(sprintf('%s: - CRC%d got %08x - calculated %08x',__METHOD__,(($crcl==2) ? 16 : 32),$incrc,$crc)); + + if ($incrc != $crc) + return self::LSZ_BADCRC; + + /* We need to read after HEX header */ + if ($readmode == self::rmHEX) { + $state = self::rhCR; + $readmode = self::rm8BIT; + + } else { + return $frametype; + } + } + + break; + + case self::rhCR: + $state = self::rhInit; + + switch ($c) { + /* we need LF after */ + case self::CR: + case self::CR|0x80: + if ($this->DEBUG) + Log::debug(sprintf('%s: ? rhCR, ignoring any remaining chars [%d]',__METHOD__,$c)); + + /* At this point, most implementations ignore checking for the remaining chars */ + return $frametype; + + /* Ok, UNIX-like EOL */ + case self::LF: + case self::LF|0x80: + $state = self::rhXON; + break; + + case self::XON: + case self::XON|0x80: + if ($this->DEBUG) + Log::debug(sprintf('%s: ? rhCR, got XON without CR/LF [%d]',__METHOD__,$c)); + + return $frametype; + + default: + Log::error(sprintf('%s: ? Something strange [%d]',__METHOD__,$c)); + return self::LSZ_BADCRC; + } + + break; + + case self::rhLF: + $state = self::rhInit; + + switch ($c) { + case self::LF: + case self::LF|0x80: + $state = self::rhXON; + break; + + default: + Log::error(sprintf('%s: ? Something strange [%d]',__METHOD__,$c)); + return self::LSZ_BADCRC; + } + + break; + + case self::rhXON: + $state = self::rhInit; + + if ($this->DEBUG) + Log::debug(sprintf('%s: rhXON',__METHOD__)); + + switch ($c) { + case self::ZPAD: + case self::ZPAD|0x80: + $state = self::rhZPAD; + $got = 0; + $crcgot = 0; + break; + + case self::XON: + case self::XON|0x80: + return $frametype; + + default: + Log::error(sprintf('%s: ! rhXON unexpcted [%d]',__METHOD__,$c)); + return self::LSZ_BADCRC; + } + + break; + } + } + + Log::error(sprintf('%s: ? Something strange or timeout [%d]',__METHOD__,$rc)); + return $rc; + } + + /** + * Internal function -- receive ZCRCW frame in 10 trys, send ZNAK/ZACK + * + * @param string $buf + * @param int $len + * @return int + */ + private function ls_zrecvcrcw(string &$buf,int $len): int + { + $trys = 0; + + do { + switch (($rc=$this->ls_zrecvdata($buf,$len,$this->ls_DataTimeout,$this->ls_Protocol&self::LSZ_OPTCRC32))) { + /* Ok, here it is */ + case self::ZCRCW: + return self::LSZ_OK; + + case self::LSZ_BADCRC: + Log::debug(sprintf('%s: - got BADCRC',__METHOD__)); + $this->ls_zsendhhdr(self::ZNAK,$this->ls_storelong(0)); + + return 1; + + case self::LSZ_TIMEOUT: + Log::debug(sprintf('%s: - got TIMEOUT',__METHOD__)); + break; + + default: + Log::error(sprintf('%s: ? Something strange [%d]',__METHOD__,$rc)); + + if ($rc < 0) + return $rc; + + $this->ls_zsendhhdr(self::ZNAK,$this->ls_storelong(0)); + + return 1; + } + + } while (++$trys < 10); + + $this->ls_zsendhhdr(self::ZNAK,$this->ls_storelong(0)); + return 1; + } + + /** + * Receive ZDATA/ZEOF frame, do 10 trys, return position + * + * @param $oldpos + * @param $pos + * @return int + */ + private function ls_zrecvnewpos($oldpos,&$pos): int + { + $rc = 0; + $trys = 0; + + do { + switch (($rc=$this->ls_zrecvhdr($this->ls_rxHdr,$this->ls_HeaderTimeout))) { + case self::ZDATA: + case self::ZEOF: + $pos = $this->ls_fetchlong($this->ls_rxHdr); + + return $rc; + + case self::ZNAK: + if (($rc=$this->ls_zsendhhdr(self::ZRPOS,$this->ls_storelong($oldpos))) < 0) + return $rc; + + break; + + case self::LSZ_TIMEOUT: + break; + + case self::LSZ_BADCRC: + $this->ls_zsendhhdr(self::ZNAK,$this->ls_storelong(0)); + + break; + + default: + Log::error(sprintf('%s: ? Something strange [%d]',__METHOD__,$rc)); + + if ($rc < 0) + return $rc; + + $this->ls_zsendhhdr(self::ZNAK,$this->ls_storelong(0)); + } + + } while (++$trys < 10); + + Log::error(sprintf('%s: ? Something strange or timeout [%d]',__METHOD__,$rc)); + return self::LSZ_TIMEOUT; + } + + /** + * Internal function to process ZRPOS + * + * @param Send $send + * @param int $newpos + * @return int + * @throws \Exception + */ + private function ls_zrpos(Send $send,int $newpos): int + { + Log::debug(sprintf('%s: + Start, newpos [%d]',__METHOD__,$newpos)); + + if ($newpos == $this->ls_txLastRepos) { + if (++$this->ls_txReposCount > 10) { + Log::error(sprintf('%s: ! ZRPOS to [%ld] limit reached',__METHOD__,$newpos)); + + return self::LSZ_ERROR; + } + + } else { + $this->ls_txReposCount = 0; + $this->ls_txLastRepos = $newpos; + } + + /* Drop window */ + $this->ls_txLastACK = $newpos; + + if (! $send->seek($newpos)) { + Log::error(sprintf('%s: ! ZRPOS to [%ld] seek error',__METHOD__,$send->filepos)); + + return self::LSZ_ERROR; + } + + if ($this->ls_txCurBlockSize > 32) + $this->ls_txCurBlockSize >>= 1; + + $this->ls_txGoodBlocks = 0; + return self::LSZ_OK; + } + + /** + * Send data block, with CRC16 and framing + * + * @param string $data + * @param int $frame + * @return int + * @throws \Exception + */ + private function ls_zsenddata(string $data,int $frame): int + { + Log::debug(sprintf('%s: + Start [%d] (%d)',__METHOD__,strlen($data),$frame)); + + if ($this->ls_Protocol&self::LSZ_OPTCRC32) { + if ($this->DEBUG) + Log::debug(sprintf('%s: - CRC32',__METHOD__)); + + $crc = self::LSZ_INIT_CRC32; + + for ($n=0;$nls_sendchar(ord($data[$n])); + $crc = $this->CRC32_UPDATE(ord($data[$n]),$crc); + } + + $this->client->buffer_add(chr(self::ZDLE).chr($frame)); + $crc = $this->CRC32_UPDATE($frame,$crc); + + /* + *chat* + if (FALSE AND $rnode->opt&self::MO_CHAT) { + if ($frame==self::ZCRCG||$frame==self::ZCRCW) { + z_devsend_c(1); + } + $this->client->buffer_add(chr(0)); + } + */ + + $crc = $this->CRC32_FINISH($crc); + $this->ls_sendchar($crc&0xff); + $crc >>= 8; + $this->ls_sendchar($crc&0xff); + $crc >>= 8; + $this->ls_sendchar($crc&0xff); + $crc >>= 8; + $this->ls_sendchar($crc&0xff); + $crc >>= 8; + + } else { + if ($this->DEBUG) + Log::debug(sprintf('%s: - CRC16',__METHOD__)); + + $crc = self::LSZ_INIT_CRC16; + + for ($n=0;$nls_sendchar(ord($data[$n])); + $crc = $this->CRC16USD_UPDATE(ord($data[$n]),$crc); + } + + $this->client->buffer_add(chr(self::ZDLE).chr($frame)); + $crc = $this->CRC16USD_UPDATE($frame,$crc); + + /* + *chat* + if (FALSE AND $rnode->opt&self::MO_CHAT) { + if ($frame==self::ZCRCG||$frame==self::ZCRCW) { + z_devsend_c(1); + } + $this->client->buffer_add(chr(0)); + } + */ + + $crc &= 0xffff; + $this->ls_sendchar($crc >> 8); + $this->ls_sendchar($crc&0xff); + } + + if (! ($this->ls_Protocol&self::LSZ_OPTDIRZAP) && self::ZCRCW == $frame) + $this->client->buffer_add(chr(self::XON)); + + return $this->client->buffer_flush($this->ls_DataTimeout); + } + + /** + * Send binary header. Use proper CRC, send var. len. if could + * + * @param int $frametype + * @param array $hdr + * @return int + * @throws \Exception + */ + private function ls_zsendbhdr(int $frametype,array $hdr): int + { + Log::debug(sprintf('%s: + Start',__METHOD__)); + + $crc = $this->LSZ_INIT_CRC(); + + /* First, calculate packet header byte */ + if (($type = self::HEADER_TYPE + [($this->ls_Protocol&self::LSZ_OPTCRC32)==self::LSZ_OPTCRC32] + [($this->ls_Protocol&self::LSZ_OPTVHDR)==self::LSZ_OPTVHDR] + [($this->ls_Protocol&self::LSZ_OPTRLE)==self::LSZ_OPTRLE]) < 0) + { + return self::LSZ_ERROR; + } + + /* Send * and packet type */ + $this->client->buffer_add(chr(self::ZPAD).chr(self::ZDLE).chr($type)); + + /* Send length of header, if needed */ + if ($this->ls_Protocol&self::LSZ_OPTVHDR) + $this->ls_sendchar(count($hdr)); + + /* Send type of frame */ + $this->ls_sendchar($frametype); + + $crc = $this->LSZ_UPDATE_CRC($frametype,$crc); + + /* Send whole header */ + for ($n=0; $nls_sendchar($hdr[$n]); + $crc = $this->LSZ_UPDATE_CRC($hdr[$n],$crc); + } + + $crc = $this->LSZ_FINISH_CRC($crc); + + if ($this->ls_Protocol&self::LSZ_OPTCRC32) { + for ($n=0;$n<4;$n++) { + $this->ls_sendchar($crc&0xff); + $crc >>= 8; + } + + } else { + $crc &= 0xffff; + $this->ls_sendchar($crc >> 8); + $this->ls_sendchar($crc&0xff); + } + + /* Clean buffer, do real send */ + return $this->client->buffer_flush($this->ls_HeaderTimeout); + } + + /** + * Send one file to peer + * + * @param Send $send + * @param int $sernum + * @param int $fileleft + * @param int $bytesleft + * @return int + * @throws \Exception + */ + private function ls_zsendfile(Send $send,int $sernum,int $fileleft,int $bytesleft): int + { + Log::debug(sprintf('%s: + Start [%s]',__METHOD__,$send->name)); + + $trys = 0; + $needack = 0; + + switch (($rc = $this->ls_zsendfinfo($send,$sernum,$send->filepos,$fileleft,$bytesleft))) { + /* Ok, It's OK! */ + case self::ZRPOS: + Log::debug(sprintf('%s: - ZRPOS to [%d]',__METHOD__,$send->filepos)); + break; + + /* Skip it */ + case self::ZSKIP: + /* Suspend it */ + case self::ZFERR: + // @todo Mark the file as skipped + Log::debug(sprintf('%s: - ZSKIP/ZFERR',__METHOD__)); + return $rc; + + case self::ZABORT: + /* Session is aborted */ + case self::ZFIN: + Log::debug(sprintf('%s: - ZABORT/ZFIN',__METHOD__)); + $this->ls_zsendhhdr(self::ZFIN,$this->ls_storelong(0)); + + return self::LSZ_ERROR; + + default: + if ($rc < 0) + return $rc; + + Log::debug(sprintf('%s: - Strange answer on ZFILE: %d',__METHOD__,$rc)); + + return self::LSZ_ERROR; + } + + /* Send file data */ + if ($this->ls_txWinSize) + $mode = ($this->ls_rxCould&self::LSZ_RXCANDUPLEX) ? self::sfSlidingWindow : self::sfBuffered; + else + $mode = self::sfStream; + + $frame = self::ZCRCW; + + while (! $send->feof()) { + /* 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: - send ZDATA at [%d]',__METHOD__,$send->filepos)); + + if (($rc=$this->ls_zsendbhdr(self::ZDATA,$this->ls_storelong($send->filepos))) < 0) + return $rc; + } + + /* Send frame of data */ + try { + $txbuf = $send->read($this->ls_txCurBlockSize); + + } catch (\Exception $e) { + Log::error(sprintf('%s: Read error')); + + return self::LSZ_ERROR; + } + + /* Select sub-frame type */ + /* This is last sub-frame -- EOF */ + if (strlen($txbuf) < $this->ls_txCurBlockSize) { + $frame = ($mode == self::sfStream) ? self::ZCRCE : self::ZCRCW; + + /* This is not-last sub-frame */ + } else { + switch ($mode) { + /* Simple sub-frame */ + case self::sfStream: + $frame = self::ZCRCG; + break; + + /* Simple sub-frame, but with SlWin */ + case self::sfSlidingWindow: + $frame = self::ZCRCQ; + break; + + case self::sfBuffered: + if (($send->filepos + strlen($txbuf)) > $this->ls_txLastACK + $this->ls_txWinSize) { + $frame = self::ZCRCW; /* Last sub-frame in buffer */ + + } else { + $frame = self::ZCRCG; /* Simple sub-frame */ + } + + break; + } + } + + if (($rc=$this->ls_zsenddata($txbuf,$frame)) < 0) + return $rc; + + /* Ok, now wait for ACKs if here is window, or sample for RPOSes */ + $trys = 0; + + do { + $needack = (self::ZCRCW == $frame) || ($this->ls_txWinSize && ($send->filepos > $this->ls_txLastACK + $this->ls_txWinSize)); + + 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: - ZSKIP',__METHOD__)); + + /* Problems occured -- suspend file */ + case self::ZFERR: + Log::debug(sprintf('%s: - ZFERR',__METHOD__)); + return $rc; + + /* Ok, position ACK */ + case self::ZACK: + $this->ls_txLastACK = $this->ls_fetchlong($this->ls_rxHdr); + Log::debug(sprintf('%s: - ZACK',__METHOD__),['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',__METHOD__)); + + if (($rc=$this->ls_zrpos($send,$this->ls_fetchlong($this->ls_rxHdr))) < 0) + return $rc; + + Log::debug(sprintf('%s: - ZRPOS [%d]',__METHOD__,$rc)); + + /* Force to retransmit ZDATA */ + $frame = self::ZCRCW; + + break; + + /* Abort transfer */ + case self::ZABORT: + /* Strange? Ok, abort too */ + case self::ZFIN: + /* Abort too */ + case self::ZCAN: + case self::LSZ_CAN: + $this->ls_zsendhhdr(self::ZFIN,$this->ls_storelong(0)); + /* Fall through */ + + case self::LSZ_RCDO: + case self::LSZ_ERROR: + return self::LSZ_ERROR; + + case self::LSZ_TIMEOUT: /* Ok! */ + break; + + default: + Log::error(sprintf('%s: ? Something strange [%d]',__METHOD__,$rc)); + + if ($rc < 0) + return $rc; + } + + } while( + /* Here is window, and we send more than window without ACK*/ + /* Frame was ZCRCW and here is no ACK for it */ + /* trys less than 10 */ + (($this->ls_txWinSize && ($send->filepos>($this->ls_txLastACK+$this->ls_txWinSize))) + || ((self::ZCRCW == $frame) && ($send->filepos>$this->ls_txLastACK))) + && ++$trys < 10); + + if ($trys >= 10) + return self::LSZ_ERROR; + + /* Ok, increase block, if here is MANY good blocks was sent */ + if (++$this->ls_txGoodBlocks > 32) { + $this->ls_txCurBlockSize <<= 1; + + if ($this->ls_txCurBlockSize > $this->ls_MaxBlockSize) + $this->ls_txCurBlockSize = $this->ls_MaxBlockSize; + + $this->ls_txGoodBlocks = 0; + } + + /* Ok, if here is EOF, send it and wait for ZRINIT or ZRPOS */ + /* We do it here, because we coulde receive ZRPOS as answer */ + if ($send->feof()) { + if (($rc=$this->ls_zsendhhdr(self::ZEOF,$this->ls_storelong($send->filepos))) < 0) + return $rc; + + $trys = 0; + + do { + switch (($rc=$this->ls_zrecvhdr($this->ls_rxHdr,$this->ls_HeaderTimeout))) { + /* They don't need this file */ + case self::ZSKIP: + /* Problems occured -- suspend file */ + case self::ZFERR: + return $rc; + + /* Repos */ + case self::ZRPOS: + if (($rc=$this->ls_zrpos($send,$this->ls_fetchlong($this->ls_rxHdr))) < 0) + return $rc; + + /* Force to retransmit ZDATA */ + $frame = self::ZCRCW; + + break; + + /* OK! */ + case self::ZRINIT: + return self::LSZ_OK; + + /* ACK for data -- it lost! */ + case self::ZACK: + Log::debug(sprintf('%s: - ZACK after EOF',__METHOD__)); + $this->ls_txLastACK = $this->ls_fetchlong($this->ls_rxHdr); + + break; + + /* Abort transfer */ + case self::ZABORT: + /* Strange? Ok, abort too */ + case self::ZFIN: + /* Abort too */ + case self::ZCAN: + return self::LSZ_ERROR; + + /* Ok, here is no header */ + case self::LSZ_TIMEOUT: + $trys++; + + break; + + default: + Log::error(sprintf('%s: ? Something strange after ZEOF [%d]',__METHOD__,$rc)); + + if ($rc < 0) + return $rc; + + $trys++; + } + + } while ($send->feof() && $trys < 10); + + if ($send->feof()) { + Log::error(sprintf('%s: ! To many tries waiting for ZEOF ACK',__METHOD__)); + + return self::LSZ_ERROR; + } + } + } + + Log::error(sprintf('%s: ? Something strange or timeout [%d]',__METHOD__,$rc)); + return $rc; + } + + /** + * Send file information to peer, get start position from them. + * Return packet type -- ZRPOS, ZSKIP, ZFERR, ZABORT or ZFIN (may be any error, too) + * + * @param int $sernum + * @param int $pos + * @return int + */ + private function ls_zsendfinfo(Send $send,int $sernum,int $pos,int $fileleft,int $bytesleft): int + { + Log::debug(sprintf('%s: + Start [%s]',__METHOD__,$send->name)); + + $trys = 0; + $retransmit = 1; + $crc = self::LSZ_INIT_CRC32; + $buf = ''; + + $this->client->buffer_clear(); + $buf = $send->sendas.chr(0); + + $buf .= sprintf('%ld %lo %o %o %ld %ld', + $send->size, + $send->mtime, + 0, + $sernum, + $fileleft, + $bytesleft + ); + + do { + if ($retransmit) { + $txHdr = []; + $txHdr[self::LSZ_F0] = (self::LSZ_CONVBIN|self::LSZ_CONVRECOV); + $txHdr[self::LSZ_F1] = 0; /* No managment */ + $txHdr[self::LSZ_F2] = 0; /* No compression/encryption */ + $txHdr[self::LSZ_F3] = 0; /* No sparse files or variable headers */ + + if (($rc=$this->ls_zsendbhdr(self::ZFILE,$txHdr)) < 0) + return $rc; + + if (($rc=$this->ls_zsenddata($buf,self::ZCRCW)) < 0) + return $rc; + + $retransmit = 0; + $trys++; + } + + switch (($rc=$this->ls_zrecvhdr($this->ls_rxHdr,$this->ls_HeaderTimeout))) { + /* Ok, he want our file */ + case self::ZRPOS: + $pos = $this->ls_fetchlong($this->ls_rxHdr); + + return self::ZRPOS; + + /* Skip */ + case self::ZSKIP: + /* Refuse */ + case self::ZFERR: + /* Check for double-skip protection */ + $sn = $this->ls_fetchlong($this->ls_rxHdr); + + /* Here is skip protection */ + if ($this->ls_SkipGuard && $sn && ($sn == $sernum-1)) { + if (($rc=$this->ls_zsendhhdr(self::ZNAK,$this->ls_storelong(0))) < 0) + return $rc; + + /* We don't need to skip this file */ + break; + + } elseif ($sn != $sernum) { + $this->ls_SkipGuard = 0; + } + /* Fall through */ + + /* Abort this session */ + case self::ZABORT: + /* Finish this session */ + case self::ZFIN: + return $rc; + + /* Send CRC to peer */ + case self::ZCRC: + $len = $this->ls_fetchlong($this->ls_rxHdr); + if (! $len) + $len = $send->size; + + $cnt = 0; + $send->seek(0); + + while (($cnt++ < $len) && (($c=$send->read(1)) > 0)) + $crc = $this->CRC32_UPDATE($c,$crc); + + $crc = $this->CRC32_FINISH($crc); + if (($rc=$this->ls_zsendhhdr(self::ZCRC,$this->ls_storelong($crc))) < 0) + return $rc; + + break; + + case self::ZRINIT: + break; + + case self::ZNAK: + case self::LSZ_TIMEOUT: + $retransmit = 1; + + break; + + case self::LSZ_BADCRC: + if (($rc=$this->ls_zsendhhdr(self::ZNAK,$this->ls_storelong(0))) < 0) + return $rc; + + break; + + default: + Log::error(sprintf('%s: ? Something strange [%d]',__METHOD__,$rc)); + + if ($rc < 0) + return $rc; + } + + } while ($trys < 10); + + Log::error(sprintf('%s: ? Something strange or timeout [%d]',__METHOD__,$rc)); + return $rc; + } + + /* Send HEX header. Use CRC16, send var. len. if could */ + private function ls_zsendhhdr(int $frametype,array $hdr): int + { + if ($this->DEBUG) + Log::debug(sprintf('%s: + Start',__METHOD__)); + + /* Send ** */ + $this->client->buffer_add(chr(self::ZPAD).chr(self::ZPAD).chr(self::ZDLE)); + + /* Send header type */ + if ($this->ls_Protocol&self::LSZ_OPTVHDR) { + $this->client->buffer_add(chr(self::ZVHEX)); + $this->ls_sendhex(count($hdr)); + + } else { + $this->client->buffer_add(chr(self::ZHEX)); + } + + $this->ls_sendhex($frametype); + $crc = $this->CRC16USD_UPDATE($frametype,self::LSZ_INIT_CRC16); + + /* Send whole header */ + for ($n=0;$nls_sendhex($hdr[$n]); + $crc = $this->CRC16USD_UPDATE((0xff&$hdr[$n]),$crc); + } + + $crc = ($crc&0xffff); + $this->ls_sendhex($crc >> 8); + $this->ls_sendhex($crc&0xff); + $this->client->buffer_add(chr(self::CR)); + $this->client->buffer_add(chr(self::LF|0x80)); + + if ($frametype != self::ZACK && $frametype != self::ZFIN) + $this->client->buffer_add(chr(self::XON)); + + /* Clean buffer, do real send */ + return $this->client->buffer_flush($this->ls_HeaderTimeout); + } + + /** + * Send ZSINIT and wait for ZACK, skip ZRINIT, ZCOMMAND, answer on ZCHALLENGE + * + * @param string $attstr + * @return int + * @throws \Exception + */ + private function ls_zsendsinit(string $attstr): int + { + Log::debug(sprintf('%s: + Start [%s]',__METHOD__,$attrstr)); + + $trys = 0; + $retransmit = 1; + + if ($attstr) { + if (strlen($attstr) > self::LSZ_MAXATTNLEN-1) + $attstr = substr($attrstr,LSZ_MAXATTNLEN); + + $this->txbuf = $attrstr; + + } else { + $this->txbuf = ''; + } + + do { + if (retransmit) { + /* We don't support ESC8, so don't ask for it in any case */ + $txHdr = []; + $txHdr[self::LSZ_F0] = ($this->ls_Protocol&self::LSZ_OPTESCAPEALL) ? self::LSZ_TXWNTESCCTL : 0; + $txHdr[self::LSZ_F1] = $txHdr[self::LSZ_F2] = $txHdr[self::LSZ_F3] = 0; + + if (($rc=$this->ls_zsendbhdr(self::ZSINIT,$txHdr)) < 0) + return $rc; + + if ($rc=$this->ls_zsenddata($this->txbuf,self::ZCRCW)) + return $rc; + + $retransmit = 0; + $trys++; + } + + switch (($rc=$this->ls_zrecvhdr($this->ls_rxHdr,$this->ls_HeaderTimeout))) { + /* Skip it */ + case self::ZRINIT: + break; + + /* Ok */ + case self::ZACK: + return self::LSZ_OK; + + /* Return number to peer, he is paranoid */ + case self::ZCHALLENGE: + if (($rc=$this->ls_zsendhhdr(self::ZACK,$this->ls_rxHdr)) < 0) + return $rc; + + break; + + case self::LSZ_BADCRC: + case self::ZCOMMAND: + if (($rc=$this->ls_zsendhhdr(self::ZNAK,$this->ls_storelong(0))) < 0) + return $rc; + + break; + + /* Retransmit */ + case ZNAK: + case LSZ_TIMEOUT: + $retransmit = 1; + + break; + + default: + Log::error(sprintf('%s: ? Something strange [%d]',__METHOD__,$rc)); + + if ($rc < 0) + return $rc; + } + + } while ($trys < 10); + + Log::error(sprintf('%s: ? Something strange or timeout [%d]',__METHOD__,$rc)); + return $rc; + } +} \ No newline at end of file diff --git a/app/Classes/Sock/SocketClient.php b/app/Classes/Sock/SocketClient.php index 34a5686..4966ef8 100644 --- a/app/Classes/Sock/SocketClient.php +++ b/app/Classes/Sock/SocketClient.php @@ -2,23 +2,199 @@ namespace App\Classes\Sock; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Str; +/** + * Class SocketClient + * + * @package App\Classes\Sock + * @property int speed + * @property int cps + */ final class SocketClient { - public \Socket $connection; // @todo make private + // For deep debugging + private bool $DEBUG = FALSE; + + private \Socket $connection; private string $address = ''; private int $port = 0; - public function __construct (\Socket $connection) { + // Our session state + private array $session = []; + + private const OK = 0; + private const EOF = -1; + private const TIMEOUT = -2; + private const RCDO = -3; + private const GCOUNT = -4; + private const ERROR = -5; + + private const TTY_SUCCESS = self::OK; + private const TTY_TIMEOUT = self::TIMEOUT; + private const TTY_HANGUP = self::RCDO; + private const TTY_ERROR = self::ERROR; + + public const TCP_SPEED = 115200; + + // Buffer for sending + private const TX_BUF_SIZE = (0x8100); + private int $tx_ptr = 0; + private int $tx_free = self::TX_BUF_SIZE; + private int $tty_status = 0; + private string $tx_buf = ''; + + // Buffer for receiving + private const RX_BUF_SIZE = (0x8100); + private int $rx_ptr = 0; + private int $rx_left = 0; + private string $rx_buf = ''; + + public function __construct (\Socket $connection,int $speed=self::TCP_SPEED) { socket_getsockname($connection,$this->address,$this->port); - Log::info(sprintf('Connection from [%s] on port [%d]',$this->address,$this->port),['m'=>__METHOD__]); + Log::info(sprintf('%s: + Connection from [%s] on port [%d]',__METHOD__,$this->address,$this->port)); $this->connection = $connection; } + public function __get($key) { + switch ($key) { + case 'cps': + case 'speed': + return Arr::get($this->session,$key); + + default: + throw new \Exception(sprintf('%s: Unknown key [%s]:',__METHOD__,$key)); + } + } + + public function __set($key,$value) { + switch ($key) { + case 'cps': + case 'speed': + return $this->session[$key] = $value; + + default: + throw new \Exception(sprintf('%s: Unknown key [%s]:',__METHOD__,$key)); + } + } + + /** + * We'll add to our transmit buffer and if doesnt have space, we'll empty it first + * + * @param string $data + * @return void + * @throws \Exception + */ + public function buffer_add(string $data): void + { + if ($this->DEBUG) + Log::debug(sprintf('%s: + Start [%s] (%d)',__METHOD__,$data,strlen($data))); + + //$rc = self::OK; + //$tx_ptr = self::TX_BUF_SIZE-$this->tx_free; + $ptr = 0; + $num_bytes = strlen($data); + $this->tty_status = self::TTY_SUCCESS; + + while ($num_bytes) { + if ($this->DEBUG) + Log::debug(sprintf('%s: - Num Bytes [%d]: TX Free [%d]',__METHOD__,$num_bytes,$this->tx_free)); + + if ($num_bytes > $this->tx_free) { + do { + $this->buffer_flush(5); + + if ($this->tty_status == self::TTY_SUCCESS) { + $n = min($this->tx_free,$num_bytes); + $this->tx_buf = substr($data,$ptr,$n); + $this->tx_free -= $n; + $num_bytes -= $n; + $ptr += $n; + } + + } while ($this->tty_status != self::TTY_SUCCESS); + + } else { + if ($this->DEBUG) + Log::debug(sprintf('%s: - Remaining data to send [%d]',__METHOD__,$num_bytes)); + + $this->tx_buf .= substr($data,$ptr,$num_bytes); + $this->tx_free -= $num_bytes; + $num_bytes = 0; + } + } + + if ($this->DEBUG) + Log::debug(sprintf('%s: = End [%s]',__METHOD__,strlen($this->tx_buf))); + } + + /** + * Clear our TX buffer + */ + public function buffer_clear(): void + { + $this->tx_buf = ''; + $this->tx_free = self::TX_BUF_SIZE; + } + + /** + * Empty our TX buffer + * + * @param int $timeout + * @return int + * @throws \Exception + */ + public function buffer_flush(int $timeout): int + { + if ($this->DEBUG) + Log::debug(sprintf('%s: + Start [%d]',__METHOD__,$timeout)); + + $rc = self::OK; + $tx_ptr = 0; + $restsize = self::TX_BUF_SIZE-$this->tx_free; + + $tm = $this->timer_set($timeout); + while (self::TX_BUF_SIZE != $this->tx_free) { + $tv = $this->timer_rest($tm); + + if ($rc = $this->canSend($tv)>0) { + if ($this->DEBUG) + Log::debug(sprintf('%s: - Sending [%d]',__METHOD__,$restsize)); + $rc = $this->send(substr($this->tx_buf,$tx_ptr,$restsize),0); + Log::debug(sprintf('%s: - Sent [%d] (%s)',__METHOD__,$rc,Str::limit($this->tx_buf,15))); + + if ($rc == $restsize) { + $this->tx_buf = ''; + $tx_ptr = 0; + $this->tx_free += $rc; + $this->buffer_clear(); + + } else if ($rc > 0) { + $tx_ptr += $rc; + $restsize -= $rc; + } + + } else { + return $rc; + } + + // @todo Enable a delay for slow clients + //sleep(1); + if ($this->timer_expired($tm)) + return self::ERROR; + } + + if ($this->DEBUG) + Log::debug(sprintf('%s: = End [%d]',__METHOD__,$rc)); + return $rc; + } + /** * @param int $timeout * @return int + * @throws \Exception */ public function canSend(int $timeout): int { @@ -38,11 +214,15 @@ final class SocketClient { /** * Create a client socket + * @param string $address + * @param int $port + * @param int $speed * @return static + * @throws SocketException */ - public static function create(string $address,int $port): self + public static function create(string $address,int $port,int $speed=self::TCP_SPEED): self { - Log::debug(sprintf('Creating connection to [%s:%d]',$address,$port)); + Log::debug(sprintf('%s: + Creating connection to [%s:%d]',__METHOD__,$address,$port)); $address = gethostbyname($address); @@ -55,13 +235,14 @@ final class SocketClient { if ($result === FALSE) throw new SocketException(SocketException::CANT_CONNECT,socket_strerror(socket_last_error($socket))); - return new self($socket); + return new self($socket,$speed); } /** * Return the client's address * * @return string + * @todo change to __get() */ public function getAddress(): string { @@ -72,6 +253,7 @@ final class SocketClient { * Return the port in use * * @return int + * @todo change to __get() */ public function getPort(): int { @@ -82,17 +264,96 @@ final class SocketClient { * @param int $timeout * @return int * @note use socketSelect() - * @todo Node used by bink yet? - * @todo to test + * @throws \Exception */ public function hasData(int $timeout): int { $read = [$this->connection]; - $write = $except = NULL; - //$rc = socket_select($read,$write,$except,$timeout); - //return $rc; - return $this->socketSelect($read,NULL,NULL,$timeout); + return $this->rx_left ?: $this->socketSelect($read,NULL,NULL,$timeout); + } + + /** + * Read data from the socket. + * If we only want 1 character, we'll return the ASCII value of the data received + * + * @param int $timeout + * @param int $len + * @return int|string + * @throws SocketException + */ + public function read(int $timeout,int $len=1024) + { + if ($this->DEBUG) + Log::debug(sprintf('%s: + Start [%d] (%d)',__METHOD__,$len,$timeout)); + + if ($timeout AND ($this->hasData($timeout) === 0)) + return ''; + + $buf = ''; + $rc = socket_recv($this->connection,$buf, $len,MSG_DONTWAIT); + if ($this->DEBUG) + Log::debug(sprintf('%s: - Read [%d]',__METHOD__,$rc)); + + if ($rc === FALSE) + throw new SocketException($x=socket_last_error($this->connection),socket_strerror($x)); + + return is_null($buf) ? '' : $buf; + } + + /** + * Read a character from the remote. + * We'll buffer everything received + * + * @param int $timeout + * @return int + * @throws SocketException + */ + public function read_ch(int $timeout): int + { + if ($this->DEBUG) + Log::debug(sprintf('%s: + Start [%d]',__METHOD__,$timeout),['rx_left'=>$this->rx_left,'rx_ptr'=>$this->rx_ptr]); + + // If our buffer is empty, we'll try and read from the remote + if ($this->rx_left == 0) { + if ($this->hasData($timeout) > 0) { + try { + if (! strlen($this->rx_buf = $this->read(0,self::RX_BUF_SIZE))) { + Log::debug(sprintf('%s: - Nothing read',__METHOD__)); + + return self::TTY_TIMEOUT; + } + + } catch (\Exception $e) { + return ($e->getCode() == 11) ? self::TTY_TIMEOUT : self::ERROR; + } + + if ($this->DEBUG) + Log::info(sprintf('%s: - Read [%d] bytes',__METHOD__,strlen($this->rx_buf))); + + $this->rx_ptr = 0; + $this->rx_left = strlen($this->rx_buf); + + } else { + return self::TTY_TIMEOUT; + } + } + + $rc = ord(substr($this->rx_buf,$this->rx_ptr,1)); + + $this->rx_left--; + $this->rx_ptr++; + + if ($this->DEBUG) + Log::debug(sprintf('%s: = Return [%x] (%c)',__METHOD__,$rc,$rc)); + + return $rc; + } + + public function rx_purge(): void + { + $this->rx_ptr = $this->rx_left = 0; + $this->rx_buf = ''; } /** @@ -102,8 +363,10 @@ final class SocketClient { * @param int $timeout * @param null $length * @return false|int + * @throws \Exception */ - public function send($message,int $timeout,$length=NULL) { + public function send($message,int $timeout,$length=NULL) + { if ($timeout AND (! $rc = $this->canSend($timeout))) return $rc; @@ -113,33 +376,77 @@ final class SocketClient { return socket_write($this->connection,$message,$length); } - private function socketSelect(?array $read,?array $write,?array $except,int $timeout): int + /** + * Set our speed + * + * @param int $value + * @todo change to __set() + */ + public function setSpeed(int $value): void { - return socket_select($read,$write,$except,$timeout); + $this->speed = $value; } /** - * Read data from the socket. - * If we only want 1 character, we'll return the ASCII value of the data received + * Wait for data on a socket * + * @param array|null $read + * @param array|null $write + * @param array|null $except * @param int $timeout - * @param int $len - * @return false|int|string|null + * @return int + * @throws \Exception */ - public function read(int $timeout,int $len=1024) + private function socketSelect(?array $read,?array $write,?array $except,int $timeout): int { - Log::debug(sprintf('+ Start [%d]',$len),['m'=>__METHOD__]); + $rc = socket_select($read,$write,$except,$timeout); - if ($timeout AND (! $rc = $this->hasData($timeout))) - return $rc; + if ($rc === FALSE) + throw new \Exception('Socket Error: '.socket_strerror(socket_last_error())); - if (($buf=socket_read($this->connection,$len,PHP_BINARY_READ)) === FALSE) { - return NULL; - } + return $rc; + } - Log::debug(sprintf(' - Read [%d]',strlen($buf)),['m'=>__METHOD__]); + /** + * Return our speed in bps + * + * @return int + * @todo change to __get() + */ + public function speed(): int + { + return $this->speed; + } - // For single character reads, we'll return the ASCII value of the buf - return ($len == 1 and (ord($buf) != 0)) ? ord($buf) : $buf; + public function timer_expired(int $timer): int + { + return (time()>=$timer); + } + + public function timer_rest(int $timer): int + { + return (($timer)-time()); + } + + public function timer_set(int $expire): int + { + return (time()+$expire); + } + + /** + * See if we there is data waiting to collect, or if we can send + * + * @param bool $read + * @param bool $write + * @param int $timeout + * @return int + * @throws \Exception + */ + public function ttySelect(bool $read,bool $write, int $timeout): int + { + $read = $read ? [$this->connection] : NULL; + $write = $write ? [$this->connection] : NULL; + + return $this->socketSelect($read,$write,NULL,$timeout); } } diff --git a/app/Classes/Sock/SocketException.php b/app/Classes/Sock/SocketException.php index d883dc9..6ea91df 100644 --- a/app/Classes/Sock/SocketException.php +++ b/app/Classes/Sock/SocketException.php @@ -8,6 +8,8 @@ final class SocketException extends \Exception { public const CANT_LISTEN = 3; public const CANT_ACCEPT = 4; public const CANT_CONNECT = 5; + public const SOCKET_ERROR = 6; + public const SOCKET_EAGAIN = 11; private array $messages = [ self::CANT_CREATE_SOCKET => 'Can\'t create socket: "%s"', @@ -15,6 +17,8 @@ final class SocketException extends \Exception { self::CANT_LISTEN => 'Can\'t listen: "%s"', self::CANT_ACCEPT => 'Can\'t accept connections: "%s"', self::CANT_CONNECT => 'Can\'t connect: "%s"', + self::SOCKET_ERROR => 'Socket Error: "%s"', + self::SOCKET_EAGAIN => 'Socket Resource Temporarily Unavailable - Try again', ]; public function __construct(int $code,string $params=NULL) { diff --git a/app/Classes/TTY.php b/app/Classes/TTY.php new file mode 100644 index 0000000..66a6264 --- /dev/null +++ b/app/Classes/TTY.php @@ -0,0 +1,918 @@ +sendf = new FileSend; // @todo these should be delcared private if they are staying here + $this->recvf = new FileReceive; + $this->rnode = new rnode; + } + + protected function check_cps(): void + { + Log::debug('- Start',['m'=>__METHOD__]); + + $cpsdelay=10; // $cpsdelay=cfgi(CFG_MINCPSDELAY); + $ncps = 38400/1000; $this->speed/1000; + $r=1; //$r=cfgi(CFG_REALMINCPS); + + if(!($this->sendf->cps=time()-$this->sendf->start)) { + $this->sendf->cps=1; + } else { + $this->sendf->cps=($this->sendf->foff-$this->sendf->soff)/$this->sendf->cps; + } + + if(!($this->recvf->cps=time()-$this->recvf->start)) { + $this->recvf->cps=1; + } else { + $this->recvf->cps=($this->recvf->foff-$this->recvf->soff)/$this->recvf->cps; + } + + if($this->sendf->start&&(true ? 0 : cfgi(CFG_MINCPSOUT))>0&&(time()-$this->sendf->start)>$cpsdelay&&$this->sendf->cps<($r?$cci:$cci*$ncps)) { + //write_log("mincpsout=%d reached, aborting session",r?cci:cci*ncps); + $tty_gothup = self::HUP_CPS; + } + + if($this->recvf->start&&(true ? 0 : cfgi(CFG_MINCPSIN))>0&&(time()-$this->recvf->start)>$cpsdelay&&$this->recvf->cps<($r?$cci:$cci*$ncps)) { + //write_log("mincpsin=%d reached, aborting session",r?cci:cci*ncps); + $tty_gothup = self::HUP_CPS; + } + + $this->getevt(); + + Log::debug('- End',['m'=>__METHOD__]); + } + + // @todo no longer used? + protected function getevt(): void + { + Log::debug('+ Start', ['m' => __METHOD__]); + $qsndbuflen = 0; + + while($this->qrecvpkt($qrcv_buf)) { + Log::debug(' - qrecvpkt Returned', ['m' => __METHOD__,'qrcv_buf'=>$qrcv_buf]); + + switch($qrcv_buf[2]) { // @todo this doesnt seem right? + case self::QR_SKIP: + //$this->rxstatus=self::RX_SKIP; + break; + + case self::QR_REFUSE: + //$this->rxstatus=self::RX_SUSPEND; + break; + + case self::QR_HANGUP: + $tty_gothup = self::HUP_OPERATOR; + break; + + case self::QR_CHAT: + /* + if($qrcv_buf[3]) { + xstrcpy((qsnd_buf+qsndbuflen),(qrcv_buf+3),self::CHAT_BUF-$qsndbuflen); + $qsndbuflen+=strlen((qrcv_buf+3)); + if($qsndbuflen>self::CHAT_BUF-128) { + $qsndbuflen=self::CHAT_BUF-128; + } + } else { + $i=$chatprot; + $chatprot=-1; + chatsend(qsnd_buf); + if($chatlg) { + chatlog_done(); + } + $chatlg=0; + $chatprot=i; + xstrcat($qsnd_buf,"\n * Chat closed\n",CHAT_BUF); + chatsend($qsnd_buf); + if($chattimer>1) { + qlcerase(); + } + $qsndbuflen=0; + $chattimer=1; + } + */ + break; + } + $qsnd_buf[$qsndbuflen]=0; + } + + if ($qsndbuflen>0) + if(! $this->chatsend($qsnd_buf)) { + $qsndbuflen=0; + } + + Log::debug('- End', ['m' => __METHOD__]); + } + + protected function qrecvpkt(&$str): int + { + Log::debug('+ Start',['m'=>__METHOD__,'str'=>$str]); + + /* + if (! $this->xsend_cb) { + return 0; + } + */ + + $rc = $this->xrecv($this->client->connection,$str,self::MSG_BUFFER-1,0); + Log::debug(sprintf(' - qrecvpkt Got [%x] (%d)',$rc,$rc),['m'=>__METHOD__]); + if ( $rc < 0 && $this->errno != 11 /*MSG_EAGAIN*/ ) { + if ($this->errno == self::ECONNREFUSED) { + $xsend_cb = NULL; + } + //DEBUG(('I',1,"can't recv (fd=%d): %s",ssock,strerror(errno))); + } + Log::debug(sprintf(' - qrecvpkt Got [%x] (%d)',$rc,$rc),['m'=>__METHOD__,'str'=>$str,'len'=>strlen($str)]); + if ($rc < 3 || ! substr($str,0,2)) { + Log::debug('+ End',['m'=>__METHOD__,'rc'=>0]); + return 0; + } + + //str[rc] = '\0'; + if (! $rc) + $str = ''; + + Log::debug('+ End',['m'=>__METHOD__,'rc'=>$rc]); + return $rc; + } + + protected function rxclose(&$f, int $what): int + { + Log::debug('+ Start',['m'=>__METHOD__,'what'=>$what,'f'=>$f]); + + $cps=time()-$this->recvf->start; + $ss = ''; + + if(!$f || !$f) { + Log::debug('= End',['m'=>__METHOD__,'rc'=>self::FOP_ERROR]); + return self::FOP_ERROR; + } + + $this->recvf->toff+=$this->recvf->foff; + $this->recvf->stot+=$this->recvf->soff; + $p2=0; + if(! $cps) { + $cps=1; + } + + $cps=($this->recvf->foff-$this->recvf->soff)/$cps; + /* + IFPerl(if((ss=perl_end_recv(what))) { + if(!$ss) { + $what=self::FOP_SKIP; + } else { + $p2 = $ss; //xstrcpy(p2,ss,MAX_PATH); + } + }); + */ + switch($what) { + case self::FOP_SUSPEND: + $ss="suspended"; + break; + case self::FOP_SKIP: + $ss="skipped"; + break; + case self::FOP_ERROR: + $ss="error"; + break; + case self::FOP_OK: + $ss="ok"; + break; + default: + $ss=""; + } + + Log::debug(' -',['m'=>__METHOD__,'soff'=>$this->recvf->soff,'ss'=>$ss]); + if($this->recvf->soff) { + //write_log("rcvd: %s, %lu bytes (from %lu), %ld cps [%s]", + // recvf.fname, (long) recvf.foff, (long) recvf.soff, cps, ss); + } else { + //write_log("rcvd: %s, %lu bytes, %ld cps [%s]", + // recvf.fname, (long) recvf.foff, cps, ss); + } + + fclose($f); + $f=''; + + /* + snprintf(p, MAX_PATH, "%s/tmp/%s", cfgs(CFG_INBOUND), recvf.fname); + if($p2) { + if($p2!='/'&&*$p2=='.') { + $ss=xstrdup($p2); + snprintf($p2,MAX_PATH,"%s/%s",cfgs(CFG_INBOUND),$ss); + xfree($ss); + } + } else { + snprintf(p2, MAX_PATH, "%s/%s", cfgs(CFG_INBOUND), recvf.fname); + } + */ + + //$ut->actime=$ut->modtime=$this->recvf->mtime; + $this->recvf->foff=0; + switch($what) { + case self::FOP_SKIP: + unlink($p); + break; + case self::FOP_SUSPEND: + case self::FOP_ERROR: + /* + if($this->whattype($p)==self::IS_PKT&&cfgi(self::CFG_KILLBADPKT)) { + unlink($p); + } else { + //utime(p,&ut); + touch ($this->recvf->name,octdec($this->recvf->mtime)); + } + */ + break; + case self::FOP_OK: + $rc=isset($receive_callback)?receive_callback($p):0; + + if($rc) { + //lunlink(p); + } else { + $ss=$p2+strlen($p2)-1; + $overwrite=0; + /* + for(i=cfgsl(CFG_ALWAYSOVERWRITE); i; i=i->next) + if(!xfnmatch(i->str,recvf.fname,FNM_PATHNAME)) { + $overwrite=1; + } + while(!$overwrite&&!stat(p2, &sb)&&p2[0]) { + if(sifname(ss)) { + ss--; + while('.' == *ss && ss >= p2) { + ss--; + } + if(ss < p2) { + write_log("can't find suitable name for %s: leaving in temporary directory",p); + p2[0] = '\x00'; + } + } + } + if(p2[0]) { + if(overwrite) { + lunlink(p2); + } + if(rename(p, p2)) { + write_log("can't rename %s to %s: %s",p,p2,strerror(errno)); + } else { + utime(p2,&ut); + chmod(p2,cfgi(CFG_DEFPERM)); + } + } + */ + Log::debug(sprintf('Recevied [%s] with mtime [%s]',$this->f->name,$this->recvf->mtime),['m'=>__METHOD__]); + touch('/tmp/tmp/'.$this->f->name,$this->recvf->mtime); + } + break; + } + + if($what==self::FOP_SKIP||$what==self::FOP_SUSPEND) { + $skipiftic=$what; + } + $this->recvf->start=0; + $this->recvf->ftot=0; + //$this->rxstatus=0; + Log::debug('= End',['m'=>__METHOD__,'rc'=>$what]); + return $what; + } + + protected function rxopen(string $name,int $rtime,int $rsize,string &$f): int + { + Log::debug('+ Start',['m'=>__METHOD__,'name'=>$name,'rtime'=>$rtime,'rsize'=>$rsize,'f'=>$f]); + + $ccs = '/tmp'; // @todo Base path needs to be a config item + $this->speed = 38400; + + $prevcps = ($this->recvf->start&&(time()-$this->recvf->start>2))?$this->recvf->cps:$this->speed/10; + + if(! $name) { + return self::FOP_ERROR; + } + + $bn = basename($name);//xstrcpy($bn, qbasename($name), self::MAX_PATH); + Log::debug(sprintf(' - bn[%s]',$bn),['m'=>__METHOD__]); + //mapname((char*)bn, cfgs(CFG_MAPIN), MAX_PATH); + + //$this->recvf->start=(int)decoct(time()); + $this->recvf->start=time(); + //xfree(recvf.fname); + $this->recvf->fname=$bn; //xstrdup($bn); + //dd(['rtime'=>$rtime,'start'=>$this->recvf->start]); + $this->recvf->mtime=$rtime; //-gmtoff($this->recvf->start); + $this->recvf->ftot=$rsize; + if($this->recvf->toff+$rsize > $this->recvf->ttot) { + $this->recvf->ttot+=$rsize; + } + + $this->recvf->nf++; + if($this->recvf->nf > $this->recvf->allf) { + $this->recvf->allf++; + } + //IFPerl(if((rc=perl_on_recv())!=FOP_OK)return rc); + /* + if($this->whattype($name)==self::IS_PKT&&($rsize==60||!$rsize)&&cfgi(self::CFG_KILLBADPKT)) { + return self::FOP_SKIP; + } + */ + $rc=$skipiftic = 0; // @todo + $skipiftic=0; + if($rc&&istic($bn)&&cfgi(self::CFG_AUTOTICSKIP)) { + //write_log($rc==self::FOP_SKIP?$weskipstr:$wesusstr,$this->recvf->fname,"auto"); + return $rc; + } + // @todo + /* + for($i=cfgsl(self::CFG_AUTOSKIP); $i; $i=$i->next) + if(!$this->xfnmatch($i->str,$bn, self::FNM_PATHNAME)) { + //write_log(weskipstr,$this->recvf.fname,""); + $skipiftic=self::FOP_SKIP; + return self::FOP_SKIP; + } + for($i=cfgsl(self::CFG_AUTOSUSPEND); $i; $i=$i->next) + if(!$this->xfnmatch($i->str, $bn, self::FNM_PATHNAME)) { + //write_log(wesusstr,$this->recvf->fname,""); + $skipiftic=self::FOP_SUSPEND; + return self::FOP_SUSPEND; + } + */ + + $p = '/tmp/tmp/'; //@todo snprintf(p, MAX_PATH, "%s/tmp/", cfgs(CFG_INBOUND)); + + //if ($sb = stat($p)) // if(stat($p, &sb)) + if(! is_dir($p) AND ! mkdir($p)) { // && $errno!=EEXIST + Log::debug(sprintf(' - dir doesnt exist and cannot make it? [%s]',$p),['m'=>__METHOD__,'rc'=>self::FOP_SUSPEND]); + //write_log("can't make directory %s: %s", p, strerror(errno)); + //write_log(wesusstr,$this->recvf.fname,""); + $skipiftic=self::FOP_SUSPEND; + return self::FOP_SUSPEND; + } + + $p = sprintf('%s/%s',$ccs,$bn);// snprintf($p, self::MAX_PATH, "%s/%s", $ccs, $bn); + + if(file_exists($p) AND ($sb=stat($p)) && $sb['size']==$rsize) {//if(!stat(p, &sb) && sb.st_size==rsize) { + Log::debug(sprintf(' - file exists and size is same? [%s]',$p),['m'=>__METHOD__,'sb'=>$sb,'rsize'=>$rsize,'rc'=>self::FOP_SKIP]); + //write_log(weskipstr,$this->recvf.fname,""); + $skipiftic=self::FOP_SKIP; + return self::FOP_SKIP; + } + + //dd(['maxpath'=>self::MAX_PATH,'bn'=>$bn]); + //snprintf($p, self::MAX_PATH, "%s/tmp/%s", $ccs, $bn); + $p = sprintf('%s/tmp/%s',$ccs,$bn); + + // If the file exists + if (file_exists($p) AND $sb=stat($p)) {//if(!stat(p, &sb)) { + Log::debug(sprintf(' - file exists... [%s]',$p),['m'=>__METHOD__,'sb'=>$sb,'rsize'=>$rsize, + 'mtime'=>$this->recvf->mtime, + //'mtime-decopt'=>(int)decoct($this->recvf->mtime), + //'mtime-octdec'=>(int)octdec($this->recvf->mtime), + 'sbmtime'=>$sb['mtime'], + 'sbmtime-decopt'=>(int)decoct($sb['mtime']), + //'sbmtime-octdec'=>(int)octdec($sb['mtime']), + ]); + // @todo binkp doesnt use octal. + if($sb['size']<$rsize && $sb['mtime']==(int)$this->recvf->mtime) { + Log::debug(sprintf(' - attempt open for append [%s]',$p),['m'=>__METHOD__]); + + $f=fopen($p, "ab"); + if(!$f) { + Log::debug(sprintf(' - attempt open for append FAILED [%s]',$p),['m'=>__METHOD__,'rc'=>self::FOP_SUSPEND]); + //write_log("can't open file %s for writing: %s", p,strerror(errno)); + //write_log(wesusstr,$this->recvf.fname,""); + $skipiftic=self::FOP_SUSPEND; + return self::FOP_SUSPEND; + } + Log::debug(sprintf(' - FTELL REPORTS [%s]',serialize(ftell($f))),['m'=>__METHOD__]); + // ftell() gives undefined results for append-only streams (opened with "a" flag). + $this->recvf->foff = $this->recvf->soff = $sb['size']; //ftell($f); + Log::debug(sprintf(' - open for append [%s] at [%d]',$p,$this->recvf->soff),['m'=>__METHOD__,'rc'=>self::FOP_CONT]); + //if(cfgi(self::CFG_ESTIMATEDTIME)) { + //write_log("start recv: %s, %lu bytes (from %lu), estimated time %s", + // $this->recvf.fname, (long) rsize, (long) $this->recvf.soff, estimatedtime(rsize-$this->recvf.soff,prevcps,effbaud)); + //} + return self::FOP_CONT; + } + } + + $f=fopen($p, "wb"); + if(!$f) { + //write_log("can't open file %s for writing: %s", p,strerror(errno)); + //write_log(wesusstr,$this->recvf.fname,""); + $skipiftic=self::FOP_SUSPEND; + return self::FOP_SUSPEND; + } + + //dd(['sb'=>$sb,'recvf'=>$this->recvf]); + Log::debug(sprintf(' - new file created [%s]',$p),['m'=>__METHOD__,'rc'=>self::FOP_OK]); + $this->recvf->foff = $this->recvf->soff = 0; + //if(cfgi(self::CFG_ESTIMATEDTIME)) { + //write_log("start recv: %s, %lu bytes, estimated time %s", + // $this->recvf.fname, (long) rsize, estimatedtime(rsize,prevcps,effbaud)); + //} + return self::FOP_OK; + } + + public function setClient(SocketClient $client): void + { + $this->client = $client; + } + + public function timer_expired(int $timer): int + { + return (time()>=$timer); + } + + public function timer_rest(int $timer): int + { + return (($timer)-time()); + } + + public function timer_set(int $expire): int + { + return (time()+$expire); + } + + protected function txclose(&$f, int $what):int + { + $cps=time()-$this->sendf->start; + + if(!$f) { + return self::FOP_ERROR; + } + $this->sendf->toff+=$this->sendf->foff; + $this->sendf->stot+=$this->sendf->soff; + + if(!$cps) { + $cps=1; + } + $cps=($this->sendf->foff-$this->sendf->soff)/$cps; + //IFPerl(perl_end_send(what)); + switch($what) { + case self::FOP_SUSPEND: + $ss="suspended"; + break; + case self::FOP_SKIP: + $ss="skipped"; + break; + case self::FOP_ERROR: + $ss="error"; + break; + case self::FOP_OK: + $ss="ok"; + break; + default: + $ss=""; + } + if($this->sendf->soff) {} + //write_log("sent: %s, %lu bytes (from %lu), %ld cps [%s]", sendf.fname, (long) sendf.foff, (long) sendf.soff, cps, ss); + else {} + //write_log("sent: %s, %lu bytes, %ld cps [%s]",sendf.fname, (long) sendf.foff, cps, ss); + $this->sendf->foff=0; + $this->sendf->ftot=0; + $this->sendf->start=0; + fclose($f); + $f=NULL; + return $what; + } + + protected function xrecv($sock,&$buf,int $len,int $wait):int + { + Log::debug('+ Start',['m'=> __METHOD__]); + + $l = 0; + + if (! $sock) { + $this->errno = self::EBADF; + return -1; + } + + if (! $wait) { + Log::debug(' - Not wait',['m'=> __METHOD__]); + $tv_tv_sec = 0; + $tv_tv_usec = 0; + $rfd = 0; //FD_ZERO(&rfd); + //FD_SET(sock, &rfd); + $read = [$sock]; + $write = []; + $except = []; + $rc = socket_select($read,$write,$except,0,0); + //$foo = ''; + //$rc = socket_recv($this->client->connection,$foo,1,MSG_PEEK | MSG_DONTWAIT); + Log::debug(' - socket_select',['m'=> __METHOD__,'rc'=>$rc,'read'=>$read,'write'=>$write,'except'=>$except]); + //$rc = $this->client->hasData(0); + if ($rc < 1) { + if (! $rc) { + $this->errno = 11; //MSG_EAGAIN; + } + + return -1; + } + } + + Log::debug(sprintf(' - doing a read now for [%d].',$len)); + $rc = socket_recv($read[0],$l,$len,MSG_PEEK | MSG_DONTWAIT); + Log::debug(' - socket_recv PEEK', ['m' => __METHOD__,'l'=>$l,'rc'=>$rc]); + + if ($rc <= 0) { + return $rc; + } + + if ($rc == 2) { + return 2; + //l = I2H16(l); +// $l = unpack('s',$l); + $l = ((ord($l[0])&0x7f)<<8)+ord($l[1]); + +// dd(['l'=>$l,'0'=>ord($l[0])&0xf,'00'=>((ord($l[0])&0x7f)<<8)+ord($l[1]),'1'=>ord($l[1]),'hex'=>sprintf('%x',unpack('v',$l)),'len'=>$len]); + if (! $l) { + return 0; + } + + if ($l > $len) { + $l = $len; + } + + Log::debug(' - L is ',['m' => __METHOD__,'l'=>min($l+$rc,$len)]); + $rc = socket_recv($sock,$buf,min($l+$rc,$len),MSG_WAITALL); + Log::debug(' - socket_recv GOT', ['m' => __METHOD__,'buf'=>$buf,'len'=>strlen($buf),'rc'=>$rc]); + if ($rc <= 0) { + return $rc; + } + $rc = min($rc - 2, strlen($buf)); + if ($rc < 1) { + return 0; + } + if ($rc >= $len) { + $rc = $len - 2; + } + $buf = substr($buf,2,$rc); //memcpy(buf, buf + 2, rc); + return $rc; + } + + return 0; + } + + private function tty_bufc(int $ch): int + { + return $this->tty_bufblock( chr($ch), 1 ); + } + + // SocketClient::buffer_add() + public function tty_bufblock(string $data, int $nbytes): int + { + Log::debug(sprintf('%s: + Start [%s] (%d)',__METHOD__,$data,$nbytes)); + $rc = self::OK; + $txptr = self::TX_BUF_SIZE - $this->tty_tx_free; + $nptr = 0; + + $this->tty_status = self::TTY_SUCCESS; + + while ( $nbytes ) { + Log::debug(sprintf(' - Num Bytes [%d]: TX Free [%d]',$nbytes,$this->tty_tx_free)); + + if ( $nbytes > $this->tty_tx_free ) { + do { + $this->tty_bufflush( 5 ); + if ( $this->tty_status == self::TTY_SUCCESS ) { + $n = min($this->tty_tx_free,$nbytes); + $this->tty_tx_buf = substr($data,$nptr,$n); + $this->tty_tx_free -= $n; + $nbytes -= $n; + $nptr += $n; + } + } while ( $this->tty_status != self::TTY_SUCCESS ); + + } else { + Log::debug(sprintf(' -'),['data'=>$data,'nptr'=>$nptr,'txptr'=>$txptr,'tx_buff'=>substr($data,$nptr+$txptr,$nbytes)]); + + $this->tty_tx_buf .= $data;// memcpy( (void *) (tty_tx_buf + txptr), nptr, nbytes ); + $this->tty_tx_free -= $nbytes; + $nbytes = 0; + } + } + + Log::debug('= End',['m'=>__METHOD__,'rc'=>$rc]); + return $rc; + } + + private function tty_bufclear(): void + { + $this->tty_tx_ptr = 0; + $this->tty_tx_free = self::TX_BUF_SIZE; + $this->tty_tx_buf = ''; + } + + protected function tty_bufflush(int $tsec): int + { + Log::debug('+ Start',['m'=>__METHOD__,'tsec'=>$tsec,'txfree'=>$this->tty_tx_free,'txptr'=>$this->tty_tx_ptr,'txbuff'=>$this->tty_tx_buf]); + + $rc = self::OK; + $restsize = self::TX_BUF_SIZE - $this->tty_tx_free - $this->tty_tx_ptr; + + $tm = $this->timer_set( $tsec ); + while (self::TX_BUF_SIZE != $this->tty_tx_free ) { + $wd = true; + $tv = $this->timer_rest( $tm ); + + if (( $rc = $this->client->canSend($tv) > 0 && $wd )) { + + Log::debug(sprintf(' - Sending [%d]: Buffer [%s] Size [%d]',substr($this->tty_tx_buf,$this->tty_tx_ptr,$restsize),$this->tty_tx_buf,$restsize)); + $rc = $this->client->send(substr($this->tty_tx_buf,$this->tty_tx_ptr,$restsize),0,$restsize); + Log::debug(sprintf(' - Sent [%d]: Buffer [%s] Size [%d]',$rc,$this->tty_tx_buf,$restsize)); + + if ($rc == $restsize ) { + $this->tty_bufclear(); + } else if ( $rc > 0 ) { + $this->tty_tx_ptr += $rc; + $restsize -= $rc; + } else if ( $rc < 0 && $this->tty_status != self::TTY_TIMEOUT ) { + return self::ERROR; + } + + } else { + return $rc; + } + + if ($this->timer_expired( $tm )) { + return self::ERROR; + } + } + + Log::debug('= End',['m'=>__METHOD__,'rc'=>$rc]); + return $rc; + } + + public function tty_getc(int $timeout): int + { + Log::debug(sprintf('%s: + Start [%d]',__METHOD__,$timeout),['rx_left'=>$this->tty_rx_left]); + + if ($this->tty_rx_left == 0 ) { + if ($this->client->hasData($timeout) > 0) { + if (! ($this->tty_rx_buf = $this->client->read(0,self::RX_BUF_SIZE))) { + Log::debug(sprintf('%s: - Nothing read',__METHOD__)); + + return ($this->EWBOEA()) ? self::TTY_TIMEOUT : self::ERROR; + } + + Log::info(sprintf('%s: - Read [%d]',__METHOD__,strlen($this->tty_rx_buf))); + $this->tty_rx_ptr = 0; + $this->tty_rx_left = strlen($this->tty_rx_buf); + + } else { + return ( $this->tty_gothup ? self::TTY_HANGUP : self::TTY_TIMEOUT ); + } + } + + $rc = ord(substr($this->tty_rx_buf,$this->tty_rx_ptr,1)); //tty_rx_buf[tty_rx_ptr++]; + + $this->tty_rx_left--; + $this->tty_rx_ptr++; + + Log::debug(sprintf('%s: = Return [%x] (%c)',__METHOD__,$rc,$rc)); + return $rc; + } + + private function tty_getc_timed(int $timeout): int + { + $t = time(); + + $rc = $this->tty_getc($timeout); + $timeout -= (time() - $t); + return $rc; + } + + protected function tty_purge(): void + { + //DEBUG(('M',3,"tty_purge")); + + $this->tty_rx_ptr = $this->tty_rx_left = 0; + /* + if ( isatty( tty_fd )) { + tio_flush_queue( tty_fd, TIO_Q_IN ); + } + */ + } + + private function tty_purgeout(): void + { + //DEBUG(('M',3,"tty_purgeout")); + + $this->tty_bufclear(); + /* + if ( isatty( tty_fd )) { + tio_flush_queue( tty_fd, TIO_Q_OUT ); + } + */ + } + + private function tty_putc(string $ch):int + { + $this->tty_bufblock($ch,1); + return $this->tty_bufflush(5); + } + + protected function tty_select($rd,$wd,int $tval): int + { + //DEBUG(('T',2,"tty_select")); + $rfd = $this->client->connection; + $wfd = $this->client->connection; + //dump($rfd,$wfd); + + //FD_ZERO( &rfd ); + //FD_ZERO( &wfd ); + //if ($rd && $rd) { + //FD_SET($tty_fd,$rfd); + $rd = FALSE; + //} + + //if ($wd && $wd ) { + //FD_SET($tty_fd,$wfd); + $wd = FALSE; + //} + + $tty_error = 0; + $read = [$this->client->connection]; + $write = [$this->client->connection]; + $except = []; + dump('calling socket_select',['timeout'=>$tval,'read'=>$read,'write'=>$write]); + $rc = socket_select($read, $write, $except,($tval ?: NULL)); + dump('done socket_select',$tval); + + $tty_error = socket_last_error(); + $tty_status = self::TTY_SUCCESS; + + if ($rc < 0 ) { + if (EWBOEA()) { + $tty_status = self::TTY_TIMEOUT; + + } else if ($errno == self::EINTR) { + $tty_status = ($tty_online && $tty_gothup ) ? self::TTY_HANGUP : self::TTY_TIMEOUT; + } else if ($errno == self::EPIPE) { + $tty_gothup = self::HUP_LINE; + $tty_status = self::TTY_HANGUP; + } else { + $tty_status = self::TTY_ERROR; + } + + } else if ($rc == 0) { + $tty_status = self::TTY_TIMEOUT; + + /* + } else { + if ($rd /*&& FD_ISSET( tty_fd, &rfd )*) { + $rd = TRUE; + } + if ($wd /*&& FD_ISSET( tty_fd, &wfd )*) { + $wd = TRUE; + } + */ + } + + //DEBUG(('T',2,"tty_select: fd=%d rc=%i (rd=%s, wd=%s)", tty_fd, rc, FDS( rd ), FDS( wd ))); + + return $rc; + } + + protected function BUFCHAR(int $c) + { + $this->tty_bufc($c); + } + + protected function BUFFLUSH(int $tsec): int + { + return $this->tty_bufflush($tsec); + } + + // @todo this should go into SocketCLient? + protected function EWBOEA(): bool + { + $errno = socket_last_error($this->client->connection); + Log::debug('+ Start',['m'=> __METHOD__,'errno'=>$errno]); + return $errno === 11 /*MSG_EAGAIN*/; + } + + protected function GETCHAR(int $t): int + { + return $this->tty_getc($t); + } + + protected function GETCHART($t): int + { + return $this->tty_getc_timed($t); + } + + public function NOTTO(string $ch): int + { + return (($ch)==self::ERROR || ($ch)==self::RCDO || ($ch)==self::EOF); + } + + protected function PUTSTR(string $s):void + { + $this->tty_bufblock($s,strlen($s)); + $this->BUFFLUSH( 5); + } + + protected function PURGEALL(): void + { + $this->tty_purge(); + $this->tty_purgeout(); + } + + protected function PUTCHAR(string $c) + { + $this->tty_putc( $c ); + } + + protected function PUTSTRCR(string $str) + { + $this->tty_bufblock($str."\r",strlen($str)+1); + return $this->tty_bufflush(5); + } +} + +class rnode +{ + public $starttime = 0; + public $options = 0; + public $netmail = 0; + public $files = 0; + public $ewboea = 0; + public $phone = ''; +} diff --git a/app/Console/Commands/BinkpReceive.php b/app/Console/Commands/BinkpReceive.php new file mode 100644 index 0000000..76b6b5e --- /dev/null +++ b/app/Console/Commands/BinkpReceive.php @@ -0,0 +1,51 @@ +setConnectionHandler([new BinkdClass,'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()); + } + } +} diff --git a/app/Console/Commands/BinkpSend.php b/app/Console/Commands/BinkpSend.php new file mode 100644 index 0000000..1b77985 --- /dev/null +++ b/app/Console/Commands/BinkpSend.php @@ -0,0 +1,48 @@ +argument('ftn')); + + $client = SocketClient::create($no->address,$no->port); + + $o = new BinkdClass; + $o->session(BinkdClass::SESSION_BINKP,$client,$no); + + Log::info(sprintf('Connection ended: %s',$client->getAddress()),['m'=>__METHOD__]); + } +} diff --git a/app/Console/Commands/EMSIReceive.php b/app/Console/Commands/EMSIReceive.php new file mode 100644 index 0000000..aa4cb7e --- /dev/null +++ b/app/Console/Commands/EMSIReceive.php @@ -0,0 +1,51 @@ +setConnectionHandler([new EMSIClass,'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()); + } + } +} diff --git a/app/Console/Commands/EMSISend.php b/app/Console/Commands/EMSISend.php new file mode 100644 index 0000000..b65cd3f --- /dev/null +++ b/app/Console/Commands/EMSISend.php @@ -0,0 +1,48 @@ +argument('ftn')); + + $client = SocketClient::create($no->address,$no->port,38400); + + $o = new EMSIClass; + $o->session(EMSIClass::SESSION_AUTO,$client,$no); + + Log::info(sprintf('Connection ended: %s',$client->getAddress()),['m'=>__METHOD__]); + } +} diff --git a/app/Console/Commands/ImportPacket.php b/app/Console/Commands/ImportPacket.php index cf7ea36..4da1f9d 100644 --- a/app/Console/Commands/ImportPacket.php +++ b/app/Console/Commands/ImportPacket.php @@ -60,7 +60,11 @@ class ImportPacket extends Command 'msgid'=>$o->msgid, ]); - $oo->area = $o->echoarea; + if (md5(utf8_decode($eo->message)) == md5($o->message)) + { + $this->warn(sprintf('Duplicate message: %s@%s with id: %s',$o->from,$o->fqfa,$o->msgid)); + break 2; + } break; @@ -150,4 +154,4 @@ class ImportPacket extends Command } } } -} \ No newline at end of file +} diff --git a/app/Console/Commands/ZmodemReceive.php b/app/Console/Commands/ZmodemReceive.php new file mode 100644 index 0000000..522f88c --- /dev/null +++ b/app/Console/Commands/ZmodemReceive.php @@ -0,0 +1,50 @@ +setConnectionHandler([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()); + } + } +} diff --git a/app/Console/Commands/ZmodemSend.php b/app/Console/Commands/ZmodemSend.php new file mode 100644 index 0000000..674fe36 --- /dev/null +++ b/app/Console/Commands/ZmodemSend.php @@ -0,0 +1,45 @@ +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->getAddress()),['m'=>__METHOD__]); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/DomainController.php b/app/Http/Controllers/DomainController.php index f24315d..3ca77f7 100644 --- a/app/Http/Controllers/DomainController.php +++ b/app/Http/Controllers/DomainController.php @@ -19,6 +19,8 @@ class DomainController extends Controller public function add_edit(Request $request,Domain $o) { if ($request->post()) { + // @todo Add validation that we can only have 1 "default" domain for a zone. + // The default domain is used when a node connects and doesnt use a full 5D address, eg: 10:999/1 vs 10:999/1@private foreach (['name','dnsdomain','active','notes'] as $key) $o->{$key} = $request->post($key); diff --git a/app/Interfaces/CRC.php b/app/Interfaces/CRC.php new file mode 100644 index 0000000..e80abaa --- /dev/null +++ b/app/Interfaces/CRC.php @@ -0,0 +1,84 @@ +'boolean', 'is_rc'=>'boolean', 'is_hub'=>'boolean', 'is_host'=>'boolean', ]; + protected $fillable = ['zone_id','host_id','node_id','point_id']; /* SCOPES */ public function scopeHost() { - + // @todo } /* RELATIONS */ @@ -26,7 +33,7 @@ class Node extends Model /** * Node nodelist flags * - * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + * @return BelongsToMany */ public function flags() { @@ -48,7 +55,7 @@ class Node extends Model public function getFTNAttribute() { return $this->zone_id - ? sprintf('%s:%s/%s.%s',$this->zone->zone_id,$this->host_id,$this->node_id,$this->point_id) + ? sprintf('%d:%d/%d.%d@%s',$this->zone->zone_id,$this->host_id,$this->node_id,$this->point_id,$this->zone->domain->name) : '-'; } @@ -63,7 +70,51 @@ class Node extends Model /* METHODS */ - public function hasFlag($relation,$model) + /** + * Find a record in the DB for a node string, eg: 10:1/1.0 + * + * @param string $ftn + * @return Node|null + * @throws Exception + */ + public static function findFTN(string $ftn): ?self + { + $matches = []; + + // @todo domain can have more chars. + if (! preg_match('#^([0-9]+):([0-9]+)/([0-9]+)(.([0-9]+))?(@([a-z]{0,8}))?$#',strtolower($ftn),$matches)) + throw new Exception('Invalid FTN: '.$ftn); + + // Check our numbers are correct. + foreach ([1,2,3] as $i) { + if (! $matches[$i] || ($matches[$i] > 0xffff)) + throw new Exception('Invalid FTN: '.$ftn); + } + if (isset($matches[5]) AND $matches[5] > 0xffff) + throw new Exception('Invalid FTN: '.$ftn); + + return (new self)->active() + ->select('nodes.*') + ->where('zones.zone_id',$matches[1]) + ->where(function($query) use ($matches) { + $query->where('hub_id',$matches[2]) + ->orWhere('host_id',$matches[2]); + }) + ->join('zones',['zones.id'=>'nodes.zone_id']) + ->join('domains',['domains.id'=>'zones.domain_id']) + ->where('zones.active',TRUE) + ->where('node_id',$matches[3]) + ->where('point_id',(isset($matches[5]) AND $matches[5]) ? $matches[5] : 0) + ->when(isset($matches[7]),function($query) use ($matches) { + $query->where('domains.name',$matches[7]); + }) + ->when((! isset($matches[7]) OR ! $matches[7]),function($query) { + $query->where('domains.default',TRUE); + }) + ->single(); + } + + public function hasFlag($relation,$model): bool { return (bool) $this->{$relation}() ->wherePivot($model->getForeignKey(),$model->{$model->getKeyName()}) diff --git a/app/Models/Setup.php b/app/Models/Setup.php new file mode 100644 index 0000000..edec7ac --- /dev/null +++ b/app/Models/Setup.php @@ -0,0 +1,125 @@ +opt_cht = 0; /* CHAT mode - not implemented*/ + $this->opt_cr = 0; /* Crypt mode - not implemented*/ + $this->opt_mb = 1; /* Multi-Batch mode */ + $this->opt_md = 0; /* CRAM-MD5 mode */ + $this->opt_nd = 0; /* http://ftsc.org/docs/fsp-1027.001: No-dupes mode */ + $this->opt_nda = 1; /* http://ftsc.org/docs/fsp-1027.001: Asymmetric ND mode */ + $this->opt_mpwd = 0; /* Multi-Password mode - not implemented */ + $this->opt_nr = 1; /* http://ftsc.org/docs/fsp-1027.001: Non-Reliable mode */ + $this->binkp_options = ['m','d','r','b']; + + /* EMSI SETTINGS */ + $this->do_prevent = 1; /* EMSI - send an immediate EMSI_INQ on connect */ + $this->ignore_nrq = 0; + $this->options = 0; /* EMSI - our capabilities */ + + /* EMSI - the order of protocols we are able to accept */ + $this->inbound = '/tmp'; + } + + /* RELATIONS */ + + public function nodes() + { + return $this->belongsToMany(Node::class); + } + + /* ATTRIBUTES */ + + public function getLocationAttribute() + { + return $this->nodes->first()->location; + } + + public function getSysopAttribute() + { + return $this->nodes->first()->sysop; + } + + public function getSystemNameAttribute() + { + return $this->nodes->first()->system; + } + + /* METHODS */ + + /** + * @throws Exception + */ + public function __get($key) + { + switch ($key) { + case 'binkp_options': + case 'ignore_nrq': + case 'inbound': + case 'opt_nr': + case 'opt_nd': + case 'opt_nda': + case 'opt_md': + case 'opt_cr': + case 'opt_mb': + case 'opt_cht': + case 'do_prevent': + case 'options': + return $this->internal[$key] ?? FALSE; + + case 'version': + return File::exists('VERSION') ? chop(File::get('VERSION')) : 'dev'; + + default: + return parent::__get($key); + } + } + + /** + * @throws Exception + */ + public function __set($key,$value) + { + switch ($key) { + case 'binkp_options': + case 'ignore_nrq': + case 'inbound': + case 'opt_nr': + case 'opt_nd': + case 'opt_nda': + case 'opt_md': + case 'opt_cr': + case 'opt_mb': + case 'opt_cht': + case 'do_prevent': + case 'options': + $this->internal[$key] = $value; + break; + + default: + parent::__get($key); + } + } +} \ No newline at end of file diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index a277818..4cda910 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider diff --git a/app/Traits/CRC.php b/app/Traits/CRC.php new file mode 100644 index 0000000..b47fb18 --- /dev/null +++ b/app/Traits/CRC.php @@ -0,0 +1,63 @@ +CRC16USD_UPDATE(ord($string[$c]),$crc); + + return $crc; + } + + private function CRC16USD_UPDATE($b,$crc): int + { + return (self::crc16usd_tab[(($crc >> 8) ^ $b) & 0xff] ^ (($crc & 0x00ff) << 8)) & 0xffff; + } + + /** + * Calculate CRC32 + * + * @param string $string + * @param bool $finish + * @return int + */ + private function CRC32(string $string,bool $finish=TRUE): int + { + $crc = 0xffffffff; + + for ($i=0;$i>8) & 0x00ffffff)) & 0xffffffff; + + return $finish ? $this->CRC32_FINISH($crc) : $crc; + } + + private function CRC32_FINISH($crc) + { + return ~$crc & 0xffffffff; + } + + private function CRC32_UPDATE($b,$crc) + { + return ((self::crc32_tab[($crc^$b) & 0xff] ^ (($crc>>8) & 0x00ffffff)) & 0xffffffff); + } + + private function LSZ_INIT_CRC() + { + return ($this->ls_Protocol & self::LSZ_OPTCRC32) ? self::LSZ_INIT_CRC32 : self::LSZ_INIT_CRC16; + } + + private function LSZ_FINISH_CRC($crc) + { + return ($this->ls_Protocol & self::LSZ_OPTCRC32) ? $this->CRC32_FINISH($crc) : $crc; + } + + private function LSZ_UPDATE_CRC($b,$crc) + { + return ($this->ls_Protocol & self::LSZ_OPTCRC32) ? $this->CRC32_UPDATE($b,$crc) : $this->CRC16USD_UPDATE($b,$crc); + } +} \ No newline at end of file diff --git a/app/Traits/ScopeActive.php b/app/Traits/ScopeActive.php index 3d43601..08f44f9 100644 --- a/app/Traits/ScopeActive.php +++ b/app/Traits/ScopeActive.php @@ -12,6 +12,6 @@ trait ScopeActive */ public function scopeActive() { - return $this->where('active',TRUE); + return $this->where($this->getTable().'.active',TRUE); } } diff --git a/database/migrations/2019_04_16_090345_create_domains.php b/database/migrations/2019_04_16_090345_create_domains.php index e5dccee..4c2a96b 100644 --- a/database/migrations/2019_04_16_090345_create_domains.php +++ b/database/migrations/2019_04_16_090345_create_domains.php @@ -19,6 +19,7 @@ class CreateDomains extends Migration $table->string('name',8)->unique(); $table->string('dnsdomain')->nullable(); $table->string('notes')->nullable(); + $table->boolean('default')->default(FALSE); $table->boolean('active'); }); } diff --git a/database/seeds/InitialSetupSeeder.php b/database/seeds/InitialSetupSeeder.php index c8e489f..d9aba02 100644 --- a/database/seeds/InitialSetupSeeder.php +++ b/database/seeds/InitialSetupSeeder.php @@ -31,6 +31,7 @@ class InitialSetupSeeder extends Seeder DB::table('domains')->insert([ 'name'=>'private', + 'default'=>TRUE, 'active'=>TRUE, ]); @@ -46,19 +47,36 @@ class InitialSetupSeeder extends Seeder DB::table('nodes')->insert([ 'zone_id'=>'1', 'host_id'=>'999', - 'node_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'=>'fidohub.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, + ]); + DB::table('setups')->insert([ 'opt_md'=>'1', ]); @@ -67,5 +85,11 @@ class InitialSetupSeeder extends Seeder 'node_id'=>'1', 'setup_id'=>'1', ]); + + DB::table('users')->insert([ + 'name'=>'Deon George', + 'email'=>'deon@leenooks.net', + 'password'=>'$2y$10$bJQDLfxnKrh6o5Sa02MZOukXcLTNQiByXSTJ7fTr.kHMpV2wxbG6.', + ]); } }