*/ class ParTCP_Outgoing_Message { public $remoteId; public $localId; public $data = []; public function __construct( $remoteId = NULL, $localId = NULL, $data = NULL ){ if ( $remoteId ){ $this->set_remote_id( $remoteId ); } if ( $localId ){ $this->set_local_id( $localId ); } if ( is_array( $data ) ){ $this->multiset( $data ); } elseif ( is_string( $data ) ){ $this->set( 'Message-Type', $data ); } } public function set_remote_id( $remoteId ){ if ( ! is_object( $remoteId ) ){ $remoteId = new ParTCP_Public_Identity( $remoteId ); } $this->remoteId = $remoteId; if ( empty( $this->data['To'] ) ){ $this->set( 'To', $this->remoteId->id ); } } public function set_local_id( $localId ){ if ( ! is_object( $localId ) ){ $localId = new ParTCP_Private_Identity( $localId ); } $this->localId = $localId; if ( empty( $this->data['From'] ) ){ $this->set( 'From', $this->localId->id ); } } public function set( $name, $value = NULL, $encrypted = FALSE ){ if ( is_null( $value ) ){ unset( $this->data[ $name ] ); return; } if ( substr( $name, -1 ) == '~' ){ $encrypted = TRUE; $name = rtrim( $name, '~' ); } if ( $encrypted ){ unset( $this->data[ $name ] ); $name .= '~'; if ( is_array( $value ) ){ $value = json_encode( $value ); } $value = trim( chunk_split( $this->encrypt( $value ), 300, "\n" ) ); } if ( is_string( $value ) ){ // remove non-UTF8 characters $value = mb_convert_encoding( $value, 'UTF-8', 'UTF-8' ); } $this->data[ $name ] = $value; } public function multiset( $namesAndValues ){ foreach ( $namesAndValues as $name => $value ){ $this->set( $name, $value ); } } public function set_date( $date = 'now' ){ $this->set( 'Date', date_create( $date ) ); } public function set_public_key(){ if ( empty( $this->localId->pubKey ) ){ throw new Exception('Missing public key'); } $this->set( 'Public-Key', $this->localId->pubKey ); } public function set_failure( $description ){ $this->set( 'Message-Type', 'failure-notice' ); $this->set( 'Failure-Description', $description ); } public function set_rejection( $code, $reason ){ $this->set( 'Message-Type', 'rejection-notice' ); $this->set( 'Rejection-Code', $code ); $this->set( 'Rejection-Reason', $reason ); } public function dump( $withSignature = NULL ){ $data = array_filter( array_replace( [ 'Message-Type' => null, 'To' => null, 'From' => null, 'Date' => null ], $this->data ), function( $v ){ return ! is_null( $v ); } ); $message = trim( yaml_emit( $data ) ); if ( substr( $message, 0, 4 ) == "---\n" ){ $message = substr( $message, 4 ); } if ( substr( $message, -4 ) == "\n..." ){ $message = substr( $message, 0, -4 ); } if ( $withSignature && empty( $this->localId->privKey ) ){ throw new Exception('Missing key for signing'); } elseif ( $withSignature || ( is_null( $withSignature ) && ! empty( $this->localId->privKey ) ) ){ ParTCP_Crypto::set_local_privkey( $this->localId->privKey ); $signature = ParTCP_Crypto::generate_signature( $message ); $message = "Signature: {$signature}\n{$message}"; } return $message; } public function send( $withSignature = NULL, $useSsl = NULL, $port = NULL ){ $message = $this->dump( $withSignature ); $host = $this->data['Via'] ?? $this->data['To'] ?? NULL; if ( ! $host ){ return FALSE; } if ( is_null( $useSsl ) ){ $useSsl = ! preg_match( '/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/', $host ) && $host != 'localhost'; } if ( is_null( $port ) ){ $port = $useSsl ? 443 : 80; } return $this->post_message( $message, $host, $useSsl, $port ); } private function post_message( $content, $host, $useSsl, $port ){ try { $connection = @fsockopen( ( $useSsl ? 'ssl://' : '' ) . $host, $port ); } catch ( Exception $e ) { return [ 'status' => -1 ]; } if ( ! $connection ){ return [ 'status' => -1 ]; } $request = 'POST / HTTP/1.1' . "\r\n" . 'Host: ' . $host . ( $port != 80 ? ':' . $port : '' ) . "\r\n" . 'Connection: close' . "\r\n" . 'Content-Type: text/plain; charset=utf-8' . "\r\n" . 'Content-Length: ' . strlen( $content ) . "\r\n" . 'X-Partcp-Version: 1.0' . "\r\n" . ( ParTCP_Crypto::$useLegacyKx ? '' : 'X-Partcp-Kx-Method: 1' . "\r\n" ) . "\r\n" . $content; fwrite( $connection, $request ); $response = ''; while ( $data = fgets( $connection ) ) { $response .= $data; } fclose( $connection ); if ( empty( $response ) ){ return [ 'status' => '', 'message' => 'Empty response from server', 'head' => '', 'body' => '' ]; } $headEnd = strpos( $response, "\r\n\r\n" ); $head = substr( $response, 0, $headEnd ); $body = substr( $response, $headEnd + 2 ); list ( $firstLine, $remainder ) = explode( "\r\n", $head, 2 ); list ( $dummy, $status, $message ) = explode( ' ', $firstLine, 3 ); if ( strpos( $remainder, "\r\nTransfer-Encoding: chunked\r\n" ) ) { $body = ''; $markerPos = $headEnd + 4; $markerLength = strpos( $response, "\r\n", $markerPos ) - $markerPos; $dataSize = hexdec( trim( substr( $response, $markerPos, $markerLength ) ) ); while ( $dataSize > 0 ) { $body .= substr( $response, $markerPos + $markerLength + 2, $dataSize ); $markerPos = $markerPos + $markerLength + 2 + $dataSize + 2; $markerLength = strpos( $response, "\r\n", $markerPos ) - $markerPos; $dataSize = hexdec( trim( substr( $response, $markerPos, $markerLength ) ) ); } } return compact( 'status', 'message', 'head', 'body' ); } private function encrypt( $value ){ static $initialized = FALSE; if ( ! $initialized ){ if ( empty( $this->localId->privKey ) || empty( $this->remoteId->pubKey ) ){ throw new Exception('Missing key for encryption'); } ParTCP_Crypto::set_remote_pubkey( $this->remoteId->pubKey ); ParTCP_Crypto::set_local_privkey( $this->localId->privKey ); $initialized = TRUE; } return ParTCP_Crypto::encrypt( $value ); } } // end of file outgoing_message.class.php