session(self::SESSION_BINKP,$client,(new Address)); $this->client->close(); Log::info(sprintf('%s: = End - Connection closed [%s]',__METHOD__,$client->address_remote)); exit(0); } 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->system->addresses->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 { if ($this->DEBUG) 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? if ($this->DEBUG) 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; if ($this->DEBUG) 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: if ($this->DEBUG) 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; } if ($this->DEBUG) 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; } if ($this->DEBUG) Log::debug(sprintf('%s: = End [%d]',__METHOD__,$rc)); return $rc; } /** * @throws Exception */ private function binkp_send(): int { if ($this->DEBUG) 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; if ($this->DEBUG) 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; } } if ($this->DEBUG) 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::info(sprintf('%s: - Parsing AKA [%s]',__METHOD__,$rem_aka)); try { if (! ($o = Address::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->system->addresses->pluck('ftn')->search($rem_aka) !== FALSE) { Log::error(sprintf('%s: ! AKA is OURS [%s]',__METHOD__,$rem_aka)); $this->msgs(self::BPM_ERR,sprintf('Sorry that is my AKA [%s]',$rem_aka)); $this->rc = self::S_FAILURE; return 0; } // @todo lock nodes $this->node->ftn = $o; // Add our mail to the queue if we have authenticated if ($this->node->aka_authed) foreach ($this->node->aka_remote as $ao) { $this->send->mail($ao); } 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->system->addresses->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)) { // Add our mail to the queue if we have authenticated if ($this->node->aka_authed) foreach ($this->node->aka_remote as $ao) { Log::debug(sprintf('%s: - Checking for any new mail to [%s]',__METHOD__,$ao->ftn)); $this->send->mail($ao); } 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($this->node->address,$file['offs']<0); return 1; } $this->recv->new($file['file']); try { switch ($this->recv->open($this->node->address,$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; } /** * Remote accepted our password * * @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"]); } }