'', 'username' => '', 'password' => '' ]; private $auth; private static $VEHILCE_INFO = '/dynamic/v1/%s'; private static $REMOTESERVICES_STATUS = '/remoteservices/v1/%s/state/execution'; private static $NAVIGATION_INFO = '/navigation/v1/%s'; private static $EFFICIENCY = '/efficiency/v1/%s'; private static $SERVICES = '/remoteservices/v1/%s/'; private static $MESSAGES = '/myinfo/v1'; private static $REMOTE_DOOR_LOCK= 'RDL'; private static $REMOTE_DOOR_UNLOCK= 'RDU'; private static $REMOTE_HORN_BLOW = "RHB"; private static $REMOTE_LIGHT_FLASH = "RLF"; private static $REMOTE_CLIMATE_NOW = "RCN"; private static $ERROR_CODE_MAPPING = [ 200 => 'OK', 401 => 'UNAUTHORIZED', 404 => 'NOT_FOUND', 405 => 'MOBILE_ACCESS_DISABLED', 408 => 'VEHICLE_UNAVAILABLE', 423 => 'ACCOUNT_LOCKED', 429 => 'TOO_MANY_REQUESTS', 500 => 'SERVER_ERROR', 503 => 'SERVICE_MAINTENANCE', ]; //public function __construct($config = null) { public function __construct($vin, $username, $password) { if (!$vin OR !$username OR !$password) throw new \Exception('Config parameters missing'); $this->auth = (object) [ 'token' => '', 'expires' => 0, 'refresh_token' => '', 'token_type' => 'Bearer', 'id_token' => '' ]; $this->_loadConfig($vin, $username, $password); if (file_exists(dirname(__FILE__).'/../core/config/devices/auth.json')) $this->auth = json_decode(file_get_contents(dirname(__FILE__).'/../core/config/devices/auth.json')); } private function _request($url, $method = 'GET', $data = null, $extra_headers = []) { $ch = curl_init(); $headers = []; // Set token if exists if ($this->auth->token && $this->auth->expires > time()) $headers[] = 'Authorization: Bearer ' . $this->auth->token; // Default CURL options curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_VERBOSE, true); curl_setopt($ch, CURLOPT_HEADER, true); // Set POST/PUT data if ($method == 'POST' || $method == 'PUT') { /*if (!$data) throw new Exception('No data provided for POST/PUT methods');*/ if ($this->auth->expires < time()) { $data_str = http_build_query($data); } else { $data_str = json_encode($data); $headers[] = 'Content-Type: application/json'; $headers[] = 'Content-Length: ' . strlen($data_str); } curl_setopt($ch, CURLOPT_POSTFIELDS, $data_str); } // Add extra headers if (count($extra_headers)) foreach ($extra_headers as $header) $headers[] = $header; curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); // Execute request $response = curl_exec($ch); if (!$response) throw new \Exception('Unable to retrieve data'); // Get response $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $header = substr($response, 0, $header_size); $body = substr($response, $header_size); curl_close($ch); return (object)[ 'headers' => $header, 'body' => json_decode($body), 'httpCode' => $this->_convertHttpCode($http_code) ]; } private function _loadConfig($vin, $username, $password) { // working with config.json file //$this->config = json_decode(file_get_contents($config)); $this->config = (object)[ 'vin' => $vin, 'username' => $username, 'password' => $password ]; } private function _saveAuth() { file_put_contents(dirname(__FILE__).'/../core/config/devices/auth.json', json_encode($this->auth)); } private function _randomCode($length = 25) { $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~'; $charactersLength = strlen($characters); $randomString = ''; for ($i = 0; $i < $length; $i++) { $randomString .= $characters[rand(0, $charactersLength - 1)]; } return $randomString; } public function getToken() { $headers = [ 'Content-Type: application/x-www-form-urlencoded', 'User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 15_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.3 Mobile/15E148 Safari/604.1' ]; $code_challenge = $this->_randomCode(86); $state = $this->_randomCode(22); // Stage 1 - Request authorization code $data = [ 'client_id' => $this->client_id, 'response_type' => 'code', 'scope' => 'openid profile email offline_access smacc vehicle_data perseus dlm svds cesim vsapi remote_services fupo authenticate_user', 'redirect_uri' => 'com.bmw.connected://oauth', 'state' => $state, 'nonce' => 'login_nonce', 'code_challenge' => $code_challenge, 'code_challenge_method' => 'plain', 'username' => $this->config->username, 'password' => $this->config->password, 'grant_type' => 'authorization_code' ]; $result = $this->_request($this->auth_url, 'POST', $data, $headers); //$stage1 = json_decode($result->body); if (!preg_match('/.*authorization=(.*)/im', $result->body->redirect_to, $matches)) throw new \Exception('Unable to get authorization token at Stage 1'); // Stage 2 - No idea, it's required to get the code $authorization = $matches[1]; $headers[] = 'Cookie: GCDMSSO=' . $authorization; $data = [ 'client_id' => $this->client_id, 'response_type' => 'code', 'scope' => 'openid profile email offline_access smacc vehicle_data perseus dlm svds cesim vsapi remote_services fupo authenticate_user', 'redirect_uri' => 'com.bmw.connected://oauth', 'state' => $state, 'nonce' => 'login_nonce', 'code_challenge'=> $code_challenge, 'code_challenge_method' => 'plain', 'authorization' => $authorization ]; $result = $this->_request($this->auth_url, 'POST', $data, $headers); if (!preg_match('/.*location:.*code=(.*?)&/im', $result->headers, $matches)) throw new \Exception('Unable to get authorization token at Stage 2'); $code = $matches[1]; // Stage 3 - Get token $headers = [ 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8', 'Authorization: Basic ' . base64_encode($this->client_id . ':' . $this->client_password) ]; $result = $this->_request($this->auth_token_url, 'POST', [ 'code' => $code, 'code_verifier' => $code_challenge, 'redirect_uri' => 'com.bmw.connected://oauth', 'grant_type' => 'authorization_code', ], $headers); //$token = json_decode($result->body); $token = $result->body; $this->auth->token = $token->access_token; $this->auth->expires = time() + $token->expires_in; $this->auth->refresh_token = $token->refresh_token; $this->auth->id_token = $token->id_token; $this->_saveAuth(); log::add('BMWConnectedDrive', 'debug', 'result ' . 'getToken OK at time ' . time() . ' and expires_in : '. serialize($token->expires_in) ); return true; } public function refreshToken() { $headers = [ 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8', 'Authorization: Basic ' . base64_encode($this->client_id . ':' . $this->client_password) ]; $result = $this->_request($this->auth_token_url, 'POST', [ 'redirect_uri' => 'com.bmw.connected://oauth', 'refresh_token' => $this->auth->refresh_token, 'grant_type' => 'refresh_token', ], $headers); // $token = json_decode($result->body); $token = $result->body; $this->auth->token = $token->access_token; $this->auth->expires = time() + $token->expires_in; $this->auth->refresh_token = $token->refresh_token; $this->auth->id_token = $token->id_token; $this->_saveAuth(); } private function _checkAuth() { if (!$this->auth->token) return $this->getToken(); if ($this->auth->token && time() > $this->auth->expires) return $this->refreshToken(); } private function _convertHttpCode($code){ return $code . " - " . $this::$ERROR_CODE_MAPPING[$code]; } public function getInfo() { $this->_checkAuth(); $result = $this->_request($this->api_url . sprintf($this::$VEHILCE_INFO, $this->config->vin)); log::add('BMWConnectedDrive', 'debug', 'result ' . 'getInfo : '. serialize($result)); return $result; } public function getRemoteServicesStatus() { $this->_checkAuth(); $result = $this->_request($this->api_url . sprintf($this::$REMOTESERVICES_STATUS, $this->config->vin), 'GET', null, ['Accept: application/json']); return $result; } public function getNavigationInfo() { $this->_checkAuth(); $result = $this->_request($this->api_url . sprintf($this::$NAVIGATION_INFO, $this->config->vin)); return $result; } public function getEfficiency() { $this->_checkAuth(); $result = $this->_request($this->api_url . sprintf($this::$EFFICIENCY, $this->config->vin)); return $result; } public function doLightFlash () { $this->_checkAuth(); $result = $this->_request($this->api_url . sprintf($this::$SERVICES, $this->config->vin) . $this::$REMOTE_LIGHT_FLASH, 'POST', null, ['Accept: application/json']); return $result; } public function doClimateNow () { $this->_checkAuth(); $result = $this->_request($this->api_url . sprintf($this::$SERVICES, $this->config->vin) . $this::$REMOTE_CLIMATE_NOW, 'POST', null, ['Accept: application/json']); return $result; } public function doDoorLock () { $this->_checkAuth(); $result = $this->_request($this->api_url . sprintf($this::$SERVICES, $this->config->vin) . $this::$REMOTE_DOOR_LOCK, 'POST', null, ['Accept: application/json']); return $result; } public function doDoorUnlock () { $this->_checkAuth(); $result = $this->_request($this->api_url . sprintf($this::$SERVICES, $this->config->vin) . $this::$REMOTE_DOOR_UNLOCK, 'POST', null, ['Accept: application/json']); return $result; } public function doHornBlow () { $this->_checkAuth(); $result = $this->_request($this->api_url . sprintf($this::$SERVICES, $this->config->vin) . $this::$REMOTE_HORN_BLOW, 'POST', null, ['Accept: application/json']); return $result; } public function doSendMessage ($title, $message) { $this->_checkAuth(); $result = $this->_request($this->api_url . $this::$MESSAGES, 'POST', ["vins"=>[$this->config->vin], "message" => $message, "subject" => $title], ['Accept: application/json']); return $result; } }