neue Parameter und devid darf keine Zeichen sondern nur Buchstaben und Zahlen enthalten *13.09.2023 -> cookie Datei und Login.dat umbenannt *19.09.2023 -> $_SERVER['SERVER_NAME'] wird wenn nicht da auf Hostname gesetzt *20.09.2023 -> get_channel_messages korrigiert (limit, offset) *26.09.2023 -> löschen von Dateien eingebaut; *31.10.2023 -> Fehler beim speichern der Logins gefixt hermine@THW PHPConnector inspiriert von https://gitlab.com/aeberhardt/stashcat-api-client curl und php >= 7.4 Funktionen: get_last_error(); login(soll der Login gespeichert werden); get_companies_list(); -> List der Firmen (wird beim Login erstellt) get_conversations_list(); -> List der Konversationen (wird beim Login erstellt) get_channels_list(); -> List der Kanäle (1. Firma) (wird beim Login erstellt) send_message_to_conversation(id,Nachricht,optional URLs als array, optional Standort als Array); send_message_to_channel(id,Nachricht,optional URLs als array, optional Standort als Array); send_message_with_file_to_conversation(id,Nachricht,Dateiinhalt (max 5MB),Dateiname in Hermine,Type Bsp:'image/jpeg',Breite Standard 0,Höhe Standard 0); send_message_with_file_to_channel(id,Nachricht,Dateiinhalt (max 5MB),Dateiname in Hermine,Type Bsp:'image/jpeg',Breite Standard 0,Höhe Standard 0); get_channel_messages(id, $_limit=50, $_offset=0); -< array get_conversation_messages(id,limit=50,offset=0); -< array Beispiele: $hermine = new hermineConnect('mailadresse','Accountpassword','Verschlüsselungskennwort'); if($hermine->login(true) !== false){ $array = $hermine->get_companies_list(); print_r($array); $array = $hermine->get_conversations_list(); print_r($array); $array = $hermine->get_channels_list(); print_r($array); $file = file_get_contents('./test.jpg'); $hermine->send_message_with_file_to_channel(165562,'jpg per php',$file,'test.jpg','image/jpeg',1080,2068); $array = $hermine->get_channel_messages(165562); foreach($array as $msg){ echo $msg->text."\n"; } $array = $hermine->search_user('Nennin'); }else{ //login fehlgeschlagen print_r($hermine->get_last_error); } */ class UUID { public static function v3($namespace, $name) { if(!self::is_valid($namespace)) return false; // Get hexadecimal components of namespace $nhex = str_replace(array('-','{','}'), '', $namespace); // Binary Value $nstr = ''; // Convert Namespace UUID to bits for($i = 0; $i < strlen($nhex); $i+=2) { $nstr .= chr(hexdec($nhex[$i].$nhex[$i+1])); } // Calculate hash value $hash = md5($nstr . $name); return sprintf('%08s-%04s-%04x-%04x-%12s', // 32 bits for "time_low" substr($hash, 0, 8), // 16 bits for "time_mid" substr($hash, 8, 4), // 16 bits for "time_hi_and_version", // four most significant bits holds version number 3 (hexdec(substr($hash, 12, 4)) & 0x0fff) | 0x3000, // 16 bits, 8 bits for "clk_seq_hi_res", // 8 bits for "clk_seq_low", // two most significant bits holds zero and one for variant DCE1.1 (hexdec(substr($hash, 16, 4)) & 0x3fff) | 0x8000, // 48 bits for "node" substr($hash, 20, 12) ); } public static function v4() { return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', // 32 bits for "time_low" mt_rand(0, 0xffff), mt_rand(0, 0xffff), // 16 bits for "time_mid" mt_rand(0, 0xffff), // 16 bits for "time_hi_and_version", // four most significant bits holds version number 4 mt_rand(0, 0x0fff) | 0x4000, // 16 bits, 8 bits for "clk_seq_hi_res", // 8 bits for "clk_seq_low", // two most significant bits holds zero and one for variant DCE1.1 mt_rand(0, 0x3fff) | 0x8000, // 48 bits for "node" mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) ); } public static function v5($namespace, $name) { if(!self::is_valid($namespace)) return false; // Get hexadecimal components of namespace $nhex = str_replace(array('-','{','}'), '', $namespace); // Binary Value $nstr = ''; // Convert Namespace UUID to bits for($i = 0; $i < strlen($nhex); $i+=2) { $nstr .= chr(hexdec($nhex[$i].$nhex[$i+1])); } // Calculate hash value $hash = sha1($nstr . $name); return sprintf('%08s-%04s-%04x-%04x-%12s', // 32 bits for "time_low" substr($hash, 0, 8), // 16 bits for "time_mid" substr($hash, 8, 4), // 16 bits for "time_hi_and_version", // four most significant bits holds version number 5 (hexdec(substr($hash, 12, 4)) & 0x0fff) | 0x5000, // 16 bits, 8 bits for "clk_seq_hi_res", // 8 bits for "clk_seq_low", // two most significant bits holds zero and one for variant DCE1.1 (hexdec(substr($hash, 16, 4)) & 0x3fff) | 0x8000, // 48 bits for "node" substr($hash, 20, 12) ); } public static function is_valid($uuid) { return preg_match('/^\{?[0-9a-f]{8}\-?[0-9a-f]{4}\-?[0-9a-f]{4}\-?'. '[0-9a-f]{4}\-?[0-9a-f]{12}\}?$/i', $uuid) === 1; } } class hermineConnect{ private $VERSION = '1.12'; private $MACHINENAME = 'default'; private $hermineServer = 'https://api.thw-messenger.de'; private $connectorID = ""; private $user = ""; private $password = ""; private $passphrase = ""; private $cURL; private $client_key = ""; private $user_id = ""; private $private_key; private $key_cache_channels; private $key_cache_conversations; private $lasterror; private $companies; private $channels; private $conversations; //private $debug = true; /* Hilfsfunktion für multipart/form-data; */ function build_data_files($boundary, $fields, $files){ $data = ''; $eol = "\r\n"; $delimiter = '-------------' . $boundary; foreach ($fields as $name => $content) { /*echo "@@\n"; print_r($content); echo "@@\n";*/ $data .= "--" . $delimiter . $eol . 'Content-Disposition: form-data; name="' . $name . "\"".$eol.$eol . $content . $eol ; } foreach ($files as $name => $content) { $data .= "--" . $delimiter . $eol . 'Content-Disposition: form-data; name="file"; filename="[object Object]"' . $eol . 'Content-Type: application/octet-stream'.$eol ; $data .= $eol; $data .= $content . $eol; } $data .= "--" . $delimiter . "--".$eol; return $data; } function __construct($user,$password,$passphrase) { function intArrayToString($ia){ $ret = ''; foreach($ia as $val){ $ret .= chr($val); } return $ret; } if(!isset($_SERVER['SERVER_NAME'])){ //wenn php ohne Apache aufgerufen wird ... exec("hostname",$ret); $_SERVER['SERVER_NAME'] = implode(" ",$ret); } $cID = array_map('hexdec', str_split('deadbeef'.sha1($_SERVER['SERVER_NAME']), 2)); $cID = intArrayToString($cID); $this->connectorID =str_replace(['+','/','='], ['','',''],base64_encode($cID)); $this->user = $user; $this->password = $password; $this->passphrase = $passphrase; $this->cURL = curl_init(); $this->MACHINENAME = $_SERVER['SERVER_NAME']; } function __destruct() { curl_close($this->cURL); } function request($_url,$_data,$_files=[]){ $_data['device_id'] = $this->connectorID; if($this->client_key != '') $_data['client_key'] = $this->client_key; $boundary = uniqid(); $delimiter = '-------------' . $boundary; $_data = $this->build_data_files($boundary, $_data, $_files); curl_setopt($this->cURL, CURLOPT_URL, $_url); curl_setopt($this->cURL, CURLOPT_VERBOSE, 0); curl_setopt($this->cURL, CURLOPT_SSL_VERIFYPEER, 1); curl_setopt($this->cURL, CURLOPT_RETURNTRANSFER, 1); curl_setopt($this->cURL, CURLOPT_POST, true); curl_setopt($this->cURL, CURLOPT_POSTFIELDS,$_data); curl_setopt($this->cURL, CURLOPT_HTTPHEADER, array("Accept: application/json, text/plain, */*", "Keep-Alive: timeout=5, max=100", "Connection: keep-alive", "Content-Type: multipart/form-data; boundary=" . $delimiter, "Content-Length: " . strlen($_data), 'Expect:' )); curl_setopt( $this->cURL, CURLOPT_COOKIESESSION, true ); curl_setopt( $this->cURL, CURLOPT_COOKIEJAR, './hermineConnectCookie.txt' ); curl_setopt( $this->cURL, CURLOPT_COOKIEFILE, './hermineConnectCookie.txt' ); curl_setopt( $this->cURL, CURLINFO_HEADER_OUT, true); if(isset($this->debug)) echo "\n\nsenddata->:\n".print_r($_data,true); $data = curl_exec($this->cURL); if(!curl_errno($this->cURL)){ if(isset($this->debug)){ echo "\n\nSendheader->:\n".print_r(curl_getinfo($this->cURL, CURLINFO_HEADER_OUT),true); echo "\n\nretdata->:\n".print_r($data,true); } $data = json_decode($data); if($data->status->value === "OK"){ $this->lasterror = ''; return $data->payload; }else{ $this->lasterror = $data; return false; } } else { $this->lasterror = 'Curl error: ' . curl_error($this->cURL); return false; } } function _check(){ //Nenninger ab 20.06.2023 neue $data = [ "app_name" => 'hermine@'.$this->MACHINENAME.'-PHP:'.$this->VERSION, "encrypted" => true, "callable" => false, "key_transfer_support" => false ]; $response = $this->request($this->hermineServer."/auth/check",$data); if($response !== false){ return true; }else{ return false; } } function _open_private_key(){ //Nenninger ab 20.06.2023 neue Parameter format=pem&type=encryption $data = ['format'=>'pem','type'=>'encryption']; $response = $this->request($this->hermineServer."/security/get_private_key",$data); if($response !== false){ if(property_exists($response,'keys')){ $private_key = json_decode($response->keys->private_key); $privkey_decoded = openssl_pkey_get_private($private_key->private, $this->passphrase); if($privkey_decoded !== false){ $this->private_key = $privkey_decoded; return true; }else{ return false; } }else{ return false; } }else{ return false; } } function _get_conversation_key($_target){ if($_target[0]=='conversation'){ if(!isset($this->key_cache_conversations[$_target[1]])){ $data = [ "conversation_id" => $_target[1] ]; $response = $this->request($this->hermineServer."/message/conversation",$data); $this->key_cache_conversations[$_target[1]] = $response->conversation->key; } if(openssl_private_decrypt(base64_decode($this->key_cache_conversations[$_target[1]]),$ret,$this->private_key,OPENSSL_PKCS1_OAEP_PADDING)) return $ret; else return false; }else if($_target[0]=='channel'){ if(!isset($this->key_cache_channels[$_target[1]])){ $data = [ "channel_id" => $_target[1], "without_members" => true ]; $response = $this->request($this->hermineServer."/channels/info",$data); $this->key_cache_channels[$_target[1]] = $response->channels->key; } if(openssl_private_decrypt(base64_decode($this->key_cache_channels[$_target[1]]),$ret,$this->private_key,OPENSSL_PKCS1_OAEP_PADDING)) return $ret; else return false; } } function _encrypt_aes($_plain, $_key, $_iv){ $ret = openssl_encrypt($_plain, "AES-256-CBC", $_key, 0, $_iv); return $ret; } function _decrypt_aes($_crypt, $_key, $_iv){ if(strlen($_crypt) == 0) return '[Dieser Inhalt wurde gelöscht.]'; $ret = openssl_decrypt($_crypt, "AES-256-CBC", $_key, 0, $_iv); if($ret === false) return '[!!ERROR DeCrypt] -> '.openssl_error_string(); return $ret; } function get_companies(){ $data = [ "no_cache" => true ]; $response = $this->request($this->hermineServer."/company/member",$data); if($response !== false){ return $response; }else{ return false; } } function get_conversations($_limit = 99999, $_offset = 0){ $data = [ "limit" => $_limit, "offset" => true, "archive" => 0 ]; $response = $this->request($this->hermineServer."/message/conversations",$data); if($response !== false){ return $response; }else{ return false; } } function get_channels($_company_id){ $data = [ "no_cache" => true, "company" => $_company_id ]; $response = $this->request($this->hermineServer."/channels/subscripted",$data); if($response !== false){ return $response; }else{ return false; } } function sendmsg($_target,$_message,$_files=[],$_url=[],$_location=NULL){ $iv = openssl_random_pseudo_bytes(16); $conversation_key = $this->_get_conversation_key($_target); $data = [ "target" => $_target[0], $_target[0]."_id" => $_target[1], "text" => bin2hex(base64_decode($this->_encrypt_aes($_message, $conversation_key, $iv))), "iv" => bin2hex($iv), "files" => json_encode($_files), //Nummern vom Upload! "url" => json_encode($_url), "type" => "text", "verification" => "", "encrypted" => true ]; if(!is_null($_location)){ $data["latitude"] = bin2hex(base64_decode($this->_encrypt_aes(strval($_location[0]), $conversation_key, $iv))); $data["longitude"] = bin2hex(base64_decode($this->_encrypt_aes(strval($_location[1]), $conversation_key, $iv))); } $response = $this->request($this->hermineServer."/message/send",$data); if($response !== false){ return $response; }else{ return false; } } function uploadfile($_target,$_fileraw,$_filename,$_filetype,$_mediawidth=0,$_mediaheight=0){ $iv = openssl_random_pseudo_bytes(16); $file_key = openssl_random_pseudo_bytes(32); $file_uuid = UUID::v4(); $chunk_size = 5 * 1024 * 1024; $filesize = strlen($_fileraw); $file_encryptet = base64_decode($this->_encrypt_aes($_fileraw, $file_key, $iv)); $file = ['dummy' => $file_encryptet]; $data = [ "resumableChunkNumber" => 0, "resumableChunkSize" => $chunk_size, "resumableCurrentChunkSize" => strlen($file_encryptet), "resumableTotalSize" => $filesize, "resumableType" => $_filetype, "resumableIdentifier" => $file_uuid, "resumableFilename" => $_filename, "resumableRelativePath" => $_filename, "resumableTotalChunks" => 1, "folder" => 0, "media_width" => $_mediawidth, "media_height" => $_mediaheight, "iv" => bin2hex($iv), "type" => $_target[0], "type_id" => $_target[1], "encrypted" => true ]; $response = $this->request($this->hermineServer."/file/upload",$data,$file); if($response !== false){ $iv = openssl_random_pseudo_bytes(16); $conversation_key = $this->_get_conversation_key($_target); $fileid = $response->file->id; $data = [ "file_id" => $fileid, "target" => $_target[0], "target_id" => $_target[1], "iv" => bin2hex($iv), "key" => bin2hex(base64_decode($this->_encrypt_aes($file_key, $conversation_key, $iv))) ]; $response = $this->request($this->hermineServer."/security/set_file_access_key",$data); return $fileid; }else{ return false; } } function get_messages($_source, $_limit=30, $_offset=0){ $conversation_key = $this->_get_conversation_key($_source); $data = [ $_source[0]."_id" => $_source[1], "source" => $_source[0], "limit" => $_limit, "offset" => $_offset, ]; $response = $this->request($this->hermineServer."/message/content",$data); $ret = []; if($response !== false){ foreach($response->messages as $message){ if($message->kind == "message"){ if($message->encrypted){ if($message->text != ''){ $encryptet = $this->_decrypt_aes(base64_encode(hex2bin($message->text)), $conversation_key, hex2bin($message->iv)); if($encryptet !== false) $message->text = $encryptet; else $message->text = '[!!decrypterror!!]'; }else{ $message->text = '[Dieser Inhalt wurde gelöscht.]'; } } if($message->location->encrypted){ $encryptet = $this->_decrypt_aes(base64_encode(hex2bin($message->location->latitude)), $conversation_key, hex2bin($message->location->iv)); if($encryptet !== false) $message->location->latitude = $encryptet; else $message->location->latitude = '[!!decrypterror!!]'; $encryptet = $this->_decrypt_aes(base64_encode(hex2bin($message->location->longitude)), $conversation_key, hex2bin($message->location->iv)); if($encryptet !== false) $message->location->longitude = $encryptet; else $message->location->longitude = '[!!decrypterror!!]'; } $ret[] = $message; } } return $ret; }else{ return false; } } function _get_channel_infos($_id){ $data = [ "channel_id" => $_id, "without_members" => false ]; $response = $this->request($this->hermineServer."/channels/info",$data); if($response !== false){ return $response->channels; }else{ return false; } } function list_uploaded_files($_type,$_search,$_offset,$_limit,$_sorting){ /*type=chats& search=& offset=0& limit=75& sorting=created_desc*/ $data = [ "type" => $_type, "search" => $_search, "offset" => $_offset, "limit" => $_limit, "sorting" => $_sorting ]; $response = $this->request($this->hermineServer."/folder/list_uploaded_files",$data); if($response !== false){ //print_r($response); return $response->files; }else{ return false; } } public function _delete_files($_ids=[]){ /*file_ids=%5B8826184%5D ([8826184])*/ $data = [ "file_ids" => json_encode($_ids) ]; $response = $this->request($this->hermineServer."/file/delete",$data); if($response !== false){ //print_r($response); return $response->success; }else{ return false; } } /* Öffentliche Funktionen */ /* Ausgabe Fehlermeldung von Hermine */ public function get_last_error(){ return $this->lasterror; } /* Ausgabe der ID (wird aus eigenem Servername ermittelt) */ public function get_connector_id(){ return $this->connectorID; } /* Login, wenn $_saveLogin=true, dann wird device_id und client_key in login.dat gespeichert und es erfolgt keine Mitteilung in Hermine */ public function login($_saveLogin=false){ $savelogin = new stdClass(); if($_saveLogin){ if(file_exists('./hermineConnectLogin.dat')){ $savelogin = json_decode(file_get_contents('./hermineConnectLogin.dat')); $this->client_key = $savelogin->ClientKey; $this->connectorID = $savelogin->DeviceId; $this->user_id = $savelogin->UserID; $this->_check(); if($this->_open_private_key()){ $this->companies = $this->get_companies(); $this->channels = $this->get_channels($this->companies->companies[0]->id); $this->conversations = $this->get_conversations(); return true; } } } $data = [ "email" => $this->user, "password" => $this->password, "app_name" => 'hermine@thw-PHP:'.$this->VERSION, "encrypted" => true, "callable" => false, "key_transfer_support" => false ]; $response = $this->request($this->hermineServer."/auth/login",$data); if($response !== false){ $this->client_key = $response->client_key; $this->user_id = $response->userinfo->id; $this->_check(); if($this->_open_private_key()){ $this->companies = $this->get_companies(); $this->channels = $this->get_channels($this->companies->companies[0]->id); $this->conversations = $this->get_conversations(); if($_saveLogin){ print_r($savelogin); $savelogin->ClientKey = $this->client_key; $savelogin->DeviceId = $this->connectorID; $savelogin->UserID = $this->user_id; file_put_contents('./hermineConnectLogin.dat',json_encode($savelogin)); } return true; } } return false; } public function get_companies_list(){ $ret = []; foreach($this->companies->companies as $company){ $ret[$company->id] = $company->name; } return $ret; } public function get_conversations_list(){ $ret = []; foreach($this->conversations->conversations as $conversation){ $ret[$conversation->id] = $conversation->members[0]->first_name.' '.$conversation->members[0]->last_name; } return $ret; } public function get_channels_list(){ $ret = []; foreach($this->channels->channels as $channels){ $ret[$channels->id] = $channels->name; } return $ret; } public function send_message_to_conversation($_conversation_id,$_message,$_url=[],$_location=NULL){ return $this->sendmsg(array('conversation',$_conversation_id),$_message,[],$_url,$_location); } public function send_message_with_file_to_conversation($_conversation_id,$_message,$_fileraw,$_filename,$_filetype,$_mediawidth=0,$_mediaheight=0){ $ret = $this->uploadfile(array('conversation',$_conversation_id),$_fileraw,$_filename,$_filetype,$_mediawidth,$_mediaheight); return $this->sendmsg(array('conversation',$_conversation_id),$_message,[$ret]); } public function send_message_to_channel($_channel_id,$_message,$_url=[],$_location=NULL){ return $this->sendmsg(array('channel',$_channel_id),$_message,[],$_url,$_location); } public function send_message_with_file_to_channel($_channel_id,$_message,$_fileraw,$_filename,$_filetype,$_mediawidth=0,$_mediaheight=0){ $ret = $this->uploadfile(array('channel',$_channel_id),$_fileraw,$_filename,$_filetype,$_mediawidth,$_mediaheight); return $this->sendmsg(array('channel',$_channel_id),$_message,[$ret]); } public function get_conversation_messages($_conversation_id, $_limit=30, $_offset=0){ return $this->get_messages(array('conversation',$_conversation_id),$_limit,$_offset); } public function get_channel_messages($_channel_id,$_limit=50, $_offset=0){ return $this->get_messages(array('channel',$_channel_id),$_limit,$_offset); } public function get_channel_infos($_channel_id){ return $this->_get_channel_infos($_channel_id); } public function search_user($_searchby, $_limit=50, $_offset=0){ $data = [ "limit" => $_limit, "offset" => $_offset, "key_hashes" => false, "search" => $_searchby, "sorting" => '["first_name_asc", "last_name_asc"]', "exclude_user_ids" => '[]', "group_ids" => '[]', ]; $response = $this->request($this->hermineServer."/users/listing",$data); if($response !== false){ return $response->users; }else{ return false; } } public function list_uploaded_chat_files($_search){ return $this->list_uploaded_files('chats',$_search,0,99999,'created_desc'); } public function delete_files($_ids=[]){ return $this->_delete_files($_ids); } } ?>