From ba3ce7cd1740468382b9f4fe4f0ef2cac37b76aa Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Thu, 4 Apr 2019 13:12:27 +0200 Subject: [PATCH] ImportSourceRestApi: new generic Import Source fixes #1818 --- .../Director/Import/ImportSourceRestApi.php | 271 +++++++++++++++ library/Director/RestApi/RestApiClient.php | 311 ++++++++++++++++++ run.php | 1 + 3 files changed, 583 insertions(+) create mode 100644 library/Director/Import/ImportSourceRestApi.php create mode 100644 library/Director/RestApi/RestApiClient.php diff --git a/library/Director/Import/ImportSourceRestApi.php b/library/Director/Import/ImportSourceRestApi.php new file mode 100644 index 00000000..1ca91127 --- /dev/null +++ b/library/Director/Import/ImportSourceRestApi.php @@ -0,0 +1,271 @@ +getSetting('url'); + $parts = \parse_url($url); + if (isset($parts['host'])) { + $host = $parts['host']; + } else { + throw new InvalidArgumentException("URL '$url' has no host"); + } + + $api = new RestApiClient( + $host, + $this->getSetting('username'), + $this->getSetting('password') + ); + if (isset($parts['path'])) { + $path = $parts['path']; + } else { + $path = '/'; + } + if (isset($parts['query'])) { + $url = "$path?" . $parts['query']; + } else { + $url = $path; + } + + $api->setScheme($this->getSetting('scheme')); + if (isset($parts['port'])) { + $api->setPort($parts['port']); + } + + $result = $api->get($url); + if ($property = $this->getSetting('extract_property')) { + if (\property_exists($result, $property)) { + $result = $result->$property; + } else { + throw new \RuntimeException(sprintf( + 'Result has no "%s" property. Available keys: %s', + $property, + \implode(', ', \array_keys((array) $result)) + )); + } + } + + return (array) $result; + } + + public function listColumns() + { + $rows = $this->fetchData(); + $columns = []; + + foreach ($rows as $object) { + foreach (array_keys((array) $object) as $column) { + if (! isset($columns[$column])) { + $columns[] = $column; + } + } + } + + return $columns; + } + + /** + * @param QuickForm $form + * @throws \Zend_Form_Exception + */ + public static function addSettingsFormFields(QuickForm $form) + { + static::addScheme($form); + static::addSslOptions($form); + static::addUrl($form); + static::addResultProperty($form); + static::addAuthentication($form); + static::addProxy($form); + } + + /** + * @param QuickForm $form + * @throws \Zend_Form_Exception + */ + protected static function addScheme(QuickForm $form) + { + $form->addElement('select', 'scheme', [ + 'label' => $form->translate('Protocol'), + 'description' => $form->translate( + 'Whether to use encryption when talking to the REST API' + ), + 'multiOptions' => [ + 'HTTPS' => $form->translate('HTTPS (strongly recommended)'), + 'HTTP' => $form->translate('HTTP (this is plaintext!)'), + ], + 'class' => 'autosubmit', + 'value' => 'HTTPS', + 'required' => true, + ]); + } + + /** + * @param QuickForm $form + * @throws \Zend_Form_Exception + */ + protected static function addSslOptions(QuickForm $form) + { + $ssl = ! ($form->getSentOrObjectSetting('scheme', 'HTTPS') === 'HTTP'); + + if ($ssl) { + static::addBoolean($form, 'ssl_verify_peer', array( + 'label' => $form->translate('Verify Peer'), + 'description' => $form->translate( + 'Whether we should check that our peer\'s certificate has' + . ' been signed by a trusted CA. This is strongly recommended.' + ) + ), 'y'); + static::addBoolean($form, 'ssl_verify_host', array( + 'label' => $form->translate('Verify Host'), + 'description' => $form->translate( + 'Whether we should check that the certificate matches the' + . 'configured host' + ) + ), 'y'); + } + } + + /** + * @param QuickForm $form + * @throws \Zend_Form_Exception + */ + protected static function addUrl(QuickForm $form) + { + $form->addElement('text', 'url', array( + 'label' => 'REST API URL', + 'description' => $form->translate( + 'Something like https://api.example.com/rest/v2/objects' + ), + 'required' => true, + )); + } + + /** + * @param QuickForm $form + * @throws \Zend_Form_Exception + */ + protected static function addResultProperty(QuickForm $form) + { + $form->addElement('text', 'extract_property', array( + 'label' => 'Extract property', + 'description' => $form->translate( + 'Often the expected result is provided in a property like "objects".' + . ' Please specify this if required' + ), + 'required' => false, + )); + } + + /** + * @param QuickForm $form + * @throws \Zend_Form_Exception + */ + protected static function addAuthentication(QuickForm $form) + { + $form->addElement('text', 'username', array( + 'label' => $form->translate('Username'), + 'description' => $form->translate( + 'Will be used for SOAP authentication against your vCenter' + ), + 'required' => true, + )); + + $form->addElement('password', 'password', array( + 'label' => $form->translate('Password'), + 'required' => true, + )); + } + + /** + * @param QuickForm $form + * @throws \Zend_Form_Exception + */ + protected static function addProxy(QuickForm $form) + { + $form->addElement('select', 'proxy_type', [ + 'label' => $form->translate('Proxy'), + 'description' => $form->translate( + 'In case your API is only reachable through a proxy, please' + . ' choose it\'s protocol right here' + ), + 'multiOptions' => $form->optionalEnum([ + 'HTTP' => $form->translate('HTTP proxy'), + 'SOCKS5' => $form->translate('SOCKS5 proxy'), + ]), + 'class' => 'autosubmit' + ]); + + $proxyType = $form->getSentOrObjectSetting('proxy_type'); + + if ($proxyType) { + $form->addElement('text', 'proxy', [ + 'label' => $form->translate('Proxy Address'), + 'description' => $form->translate( + 'Hostname, IP or :' + ), + 'required' => true, + ]); + if ($proxyType === 'HTTP') { + $form->addElement('text', 'proxy_user', [ + 'label' => $form->translate('Proxy Username'), + 'description' => $form->translate( + 'In case your proxy requires authentication, please' + . ' configure this here' + ), + ]); + + $passRequired = strlen($form->getSentOrObjectSetting('proxy_user')) > 0; + + $form->addElement('password', 'proxy_pass', [ + 'label' => $form->translate('Proxy Password'), + 'required' => $passRequired + ]); + } + } + } + + /** + * @param QuickForm $form + * @param string $key + * @param array $options + * @param string|null $default + * @throws \Zend_Form_Exception + */ + protected static function addBoolean(QuickForm $form, $key, $options, $default = null) + { + if ($default === null) { + $form->addElement('OptionalYesNo', $key, $options); + } else { + $form->addElement('YesNo', $key, $options); + $form->getElement($key)->setValue($default); + } + } + + /** + * @param QuickForm $form + * @param string $key + * @param string $label + * @param string $description + * @throws \Zend_Form_Exception + */ + protected static function optionalBoolean(QuickForm $form, $key, $label, $description) + { + static::addBoolean($form, $key, array( + 'label' => $label, + 'description' => $description + )); + } +} diff --git a/library/Director/RestApi/RestApiClient.php b/library/Director/RestApi/RestApiClient.php new file mode 100644 index 00000000..2ebc4d47 --- /dev/null +++ b/library/Director/RestApi/RestApiClient.php @@ -0,0 +1,311 @@ + CURLPROXY_HTTP, + 'SOCKS5' => CURLPROXY_SOCKS5, + ]; + + /** + * RestApiClient constructor. + * + * Please note that only the host is required, user and pass are optional + * + * @param string $host + * @param string|null $user + * @param string|null $pass + */ + public function __construct($host, $user = null, $pass = null) + { + $this->host = $host; + $this->user = $user; + $this->pass = $pass; + } + + /** + * Use a proxy + * + * @param $url + * @param string $type Either HTTP or SOCKS5 + * @return $this + */ + public function setProxy($url, $type = 'HTTP') + { + $this->proxy = $url; + if (\is_int($type)) { + $this->proxyType = $type; + } else { + $this->proxyType = $this->proxyTypes[$type]; + } + return $this; + } + + /** + * @param string $user + * @param string $pass + * @return $this + */ + public function setProxyAuth($user, $pass) + { + $this->proxyUser = $user; + $this->proxyPass = $pass; + return $this; + } + + /** + * @return string + */ + public function getScheme() + { + if ($this->scheme === null) { + return 'HTTPS'; + } else { + return $this->scheme; + } + } + + public function setScheme($scheme) + { + $scheme = \strtoupper($scheme); + if (! \in_array($scheme, ['HTTP', 'HTTPS'])) { + throw new InvalidArgumentException("Got invalid scheme: $scheme"); + } + + $this->scheme = $scheme; + return $this; + } + + /** + * @return string + */ + public function getPort() + { + if ($this->port === null) { + return $this->getScheme() === 'HTTPS' ? 443 : 80; + } else { + return $this->port; + } + } + + /** + * @param int|string|null $port + * @return $this + */ + public function setPort($port) + { + if ($port === null) { + $this->port = null; + return $this; + } + $port = (int) ($port); + if ($port < 1 || $port > 65535) { + throw new InvalidArgumentException("Got invalid port: $port"); + } + + $this->port = $port; + return $this; + } + + /** + * @return bool + */ + public function isDefaultPort() + { + return $this->port === null + || $this->getScheme() === 'HTTPS' && $this->getPort() === 443 + || $this->getScheme() === 'HTTP' && $this->getPort() === 80; + } + + /** + * @param bool $disable + * @return $this + */ + public function disableSslPeerVerification($disable = true) + { + $this->verifySslPeer = ! $disable; + return $this; + } + + /** + * @param bool $disable + * @return $this + */ + public function disableSslHostVerification($disable = true) + { + $this->verifySslHost = ! $disable; + return $this; + } + + /** + * @param string $url + * @return string + */ + public function url($url) + { + return \sprintf( + '%s://%s%s/%s', + \strtolower($this->getScheme()), + $this->host, + $this->isDefaultPort() ? '' : ':' . $this->getPort(), + ltrim($url, '/') + ); + } + + /** + * @param string $url + * @param mixed $body + * @param array $headers + * @return mixed + */ + public function get($url, $body = null, $headers = []) + { + return $this->request('get', $url, $body, $headers); + } + + /** + * @param $url + * @param null $body + * @param array $headers + * @return mixed + */ + public function post($url, $body = null, $headers = []) + { + return $this->request('post', $url, Json::encode($body), $headers); + } + + /** + * @param $method + * @param $url + * @param null $body + * @param array $headers + * @return mixed + */ + protected function request($method, $url, $body = null, $headers = []) + { + $sendHeaders = ['Host: ' . $this->host]; + foreach ($headers as $key => $val) { + $sendHeaders[] = "$key: $val"; + } + + if (! \in_array('Accept', $headers)) { + $sendHeaders[] = 'Accept: application/json'; + } + + $url = $this->url($url); + $opts = [ + CURLOPT_URL => $url, + CURLOPT_HTTPHEADER => $sendHeaders, + CURLOPT_CUSTOMREQUEST => \strtoupper($method), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CONNECTTIMEOUT => 5, + ]; + + if ($this->getScheme() === 'HTTPS') { + $opts[CURLOPT_SSL_VERIFYPEER] = $this->verifySslPeer; + $opts[CURLOPT_SSL_VERIFYHOST] = $this->verifySslHost ? 2 : 0; + } + + if ($this->user !== null) { + $opts[CURLOPT_USERPWD] = \sprintf('%s:%s', $this->user, $this->pass); + } + + if ($this->proxy) { + $opts[CURLOPT_PROXY] = $this->proxy; + $opts[CURLOPT_PROXYTYPE] = $this->proxyType; + + if ($this->proxyUser) { + $opts['CURLOPT_PROXYUSERPWD'] = \sprintf( + '%s:%s', + $this->proxyUser, + $this->proxyPass + ); + } + } + + if ($body !== null) { + $opts[CURLOPT_POSTFIELDS] = $body; + } + + $curl = $this->curl(); + \curl_setopt_array($curl, $opts); + + $res = \curl_exec($curl); + if ($res === false) { + throw new RuntimeException('CURL ERROR: ' . \curl_error($curl)); + } + + $statusCode = \curl_getinfo($curl, CURLINFO_HTTP_CODE); + if ($statusCode === 401) { + throw new RuntimeException( + 'Unable to authenticate, please check your REST API credentials' + ); + } + + if ($statusCode >= 400) { + throw new RuntimeException( + "Got $statusCode: " . \var_export($res, 1) + ); + } + + return Json::decode($res); + } + + /** + * @return resource + */ + protected function curl() + { + if ($this->curl === null) { + $this->curl = \curl_init(\sprintf('https://%s:%d', $this->host, $this->port)); + if (! $this->curl) { + throw new RuntimeException('CURL INIT ERROR: ' . \curl_error($this->curl)); + } + } + + return $this->curl; + } +} diff --git a/run.php b/run.php index 7e762545..ec074f60 100644 --- a/run.php +++ b/run.php @@ -17,6 +17,7 @@ $this->provideHook('monitoring/ServiceActions'); $this->provideHook('director/ImportSource', $prefix . 'Import\\ImportSourceSql'); $this->provideHook('director/ImportSource', $prefix . 'Import\\ImportSourceLdap'); $this->provideHook('director/ImportSource', $prefix . 'Import\\ImportSourceCoreApi'); +$this->provideHook('director/ImportSource', $prefix . 'Import\\ImportSourceRestApi'); $this->provideHook('director/DataType', $prefix . 'DataType\\DataTypeArray'); $this->provideHook('director/DataType', $prefix . 'DataType\\DataTypeBoolean');