diff --git a/.gitignore b/.gitignore index fd29596..c099e65 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,8 @@ # OS X .DS_Store + +# Generated documentation +docs/ + +# Local settings +*.local.php diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..afa0e30 --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +.PHONY: docs + +docs: + phpdoc -d Splunk -t docs diff --git a/README.md b/README.md index 9282e49..8994a43 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ into the repository with git: ## Requirements -The SDK requires PHP 5.3+ +The SDK requires PHP 5.2+ ## ... diff --git a/Splunk.php b/Splunk.php new file mode 100644 index 0000000..ffa3105 --- /dev/null +++ b/Splunk.php @@ -0,0 +1,30 @@ + (optional) The username to login with. Defaults to "admin". + * 'password' => (optional) The password to login with. Defaults to "changeme". + * 'host' => (optional) The hostname of the Splunk server. Defaults to "localhost". + * 'port' => (optional) The port of the Splunk server. Defaults to 8089. + * 'scheme' => (optional) The scheme to use: either "http" or "https". Defaults to "https". + * 'http' => (optional) An Http object that will be used for performing HTTP requests. + * } + */ + public function __construct($args) + { + $args = array_merge(array( + 'username' => 'admin', + 'password' => 'changeme', + 'host' => 'localhost', + 'port' => 8089, + 'scheme' => 'https', + 'http' => new Splunk_Http(), + ), $args); + + $this->username = $args['username']; + $this->password = $args['password']; + $this->host = $args['host']; + $this->port = $args['port']; + $this->scheme = $args['scheme']; + $this->http = $args['http']; + } + + // === Operations === + + /** + * Authenticates to the Splunk server. + */ + public function login() + { + $response = $this->http->post($this->url('/services/auth/login'), array( + 'username' => $this->username, + 'password' => $this->password, + )); + + $sessionKey = Splunk_Util::getTextContentAtXpath( + new SimpleXMLElement($response['body']), + '/response/sessionKey'); + + $this->token = 'Splunk ' . $sessionKey; + } + + // === Accessors === + + /** + * Returns the token used to authenticate HTTP requests + * after logging in. + */ + public function getToken() + { + return $this->token; + } + + // === Utility === + + private function url($path) + { + return "{$this->scheme}://{$this->host}:{$this->port}{$path}"; + } +} diff --git a/Splunk/Http.php b/Splunk/Http.php new file mode 100644 index 0000000..182c060 --- /dev/null +++ b/Splunk/Http.php @@ -0,0 +1,154 @@ +request('get', $url); + } + + public function post($url, $params=array()) + { + return $this->request('post', $url, http_build_query($params)); + } + + /** + * @param string $method HTTP request method (ex: 'get'). + * @param string $url URL to fetch. + * @param string $request_body content to send in the request. + * @return object { + * 'status' => HTTP status code (ex: 200). + * 'reason' => HTTP reason string (ex: 'OK'). + * 'headers' => Dictionary of headers. (ex: array('Content-Length' => '0')). + * 'body' => Content of the response. + * } + */ + private function request($method, $url, $request_body='') + { + $opts = array( + CURLOPT_HTTPGET => TRUE, + CURLOPT_URL => $url, + CURLOPT_TIMEOUT => 60, // secs + CURLOPT_RETURNTRANSFER => TRUE, + CURLOPT_HEADER => TRUE, + // disable SSL certificate validation + CURLOPT_SSL_VERIFYPEER => FALSE, + ); + + switch ($method) + { + case 'get': + $opts[CURLOPT_HTTPGET] = TRUE; + break; + case 'post': + $opts[CURLOPT_POST] = TRUE; + $opts[CURLOPT_POSTFIELDS] = $request_body; + break; + default: + $opts[CURLOPT_CUSTOMREQUEST] = strtoupper($method); + break; + } + + if (!($curl = curl_init())) + throw new Splunk_ConnectException('Unable to initialize cURL.'); + if (!(curl_setopt_array($curl, $opts))) + throw new Splunk_ConnectException(curl_error($curl)); + if (!($response = curl_exec($curl))) + throw new Splunk_ConnectException(curl_error($curl)); + + $status = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + $header_size = curl_getinfo($curl, CURLINFO_HEADER_SIZE); + $header_text = substr($response, 0, $header_size); + $body = substr($response, $header_size); + + $headers = array(); + $header_lines = explode("\r\n", trim($header_text)); + $status_line = array_shift($header_lines); + foreach ($header_lines as $line) + { + list($key, $value) = explode(':', $line, 2); + $headers[$key] = trim($value); + } + + list($http_version, $_, $reason) = explode(' ', $status_line, 3); + + $response = array( + 'status' => $status, + 'reason' => $reason, + 'headers' => $headers, + 'body' => $body, + ); + + if ($status >= 400) + throw new Splunk_HttpException($response); + else + return $response; + } +} + +/** + * Thrown when unable to connect to a Splunk server. + * + * @package Splunk + */ +class Splunk_ConnectException extends Exception {} + +/** + * Thrown when an HTTP request fails due to a non 2xx status code. + * + * @package Splunk + */ +class Splunk_HttpException extends Exception +{ + private $response; + + // === Init === + + public function __construct($response) + { + $detail = Splunk_HttpException::parseFirstMessageFrom($response); + + // FIXME: Include HTTP "reason" in message + $message = "HTTP {$response['status']} {$response['reason']}"; + if ($detail != NULL) + $message .= ' -- ' . $detail; + + $this->response = $response; + parent::__construct($message); + } + + private static function parseFirstMessageFrom($response) + { + return Splunk_Util::getTextContentAtXpath( + new SimpleXMLElement($response['body']), + '/response/messages/msg'); + } + + // === Accessors === + + public function getResponse() + { + return $this->response; + } +} diff --git a/Splunk/Util.php b/Splunk/Util.php new file mode 100644 index 0000000..afdac71 --- /dev/null +++ b/Splunk/Util.php @@ -0,0 +1,48 @@ +xpath($xpathExpr); + return (count($matchingElements) == 0) + ? NULL + : Splunk_Util::getTextContentOfXmlElement($matchingElements[0]); + } + + /** + * @param SimpleXMLElement $xmlElement + * @return string + */ + private static function getTextContentOfXmlElement($xmlElement) + { + // HACK: Some versions of PHP 5 can't access the [0] element + // of a SimpleXMLElement object properly. + return (string) $xmlElement; + } +} diff --git a/tests/ContextTest.php b/tests/ContextTest.php new file mode 100644 index 0000000..4d71ccf --- /dev/null +++ b/tests/ContextTest.php @@ -0,0 +1,85 @@ + 200, + 'reason' => 'OK', + 'headers' => array(), + 'body' => ' + +068b3021210eb4b67819b1a292302948 +'); + + $http = $this->getMock('Splunk_Http'); + $http->expects($this->once()) + ->method('post') + ->will($this->returnValue($http_response)); + + $context = new Splunk_Context(array( + 'http' => $http, + )); + $context->login(); + + $this->assertEquals( + 'Splunk 068b3021210eb4b67819b1a292302948', + $context->getToken()); + } + + /** + * @expectedException Splunk_HttpException + * @expectedExceptionMessage Login failed + */ + public function testLoginFailDueToBadPassword() + { + $http_response = array( + 'status' => 401, + 'reason' => 'Unauthorized', + 'headers' => array(), + 'body' => ' + + +Login failed + +'); + + $http = $this->getMock('Splunk_Http'); + $http->expects($this->once()) + ->method('post') + ->will($this->throwException( + new Splunk_HttpException($http_response))); + + $context = new Splunk_Context(array( + 'http' => $http, + )); + $context->login(); + } + + public function testLoginSuccessOnRealServer() + { + global $Splunk_testSettings; + + $context = new Splunk_Context($Splunk_testSettings['connectArgs']); + $context->login(); + } +} diff --git a/tests/HttpTest.php b/tests/HttpTest.php new file mode 100644 index 0000000..4966c8b --- /dev/null +++ b/tests/HttpTest.php @@ -0,0 +1,30 @@ +get('http://www.splunk.com/'); + + $this->assertEquals(200, $response['status']); + $this->assertContains('', $response['body']); + } +} diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 0000000..4db1caa --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,2 @@ +test: + phpunit . diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..6603513 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,13 @@ +## Requirements + +* PHPUnit 3.6 + +## Running the Tests + +Navigate to this directory, then run: + + phpunit . + +Or alternatively: + + make \ No newline at end of file diff --git a/tests/settings.default.php b/tests/settings.default.php new file mode 100644 index 0000000..83c31a4 --- /dev/null +++ b/tests/settings.default.php @@ -0,0 +1,13 @@ + 'localhost', + //'port' => 8089, + //'username' => 'admin', + 'password' => 'changeme', +); diff --git a/tests/settings.php b/tests/settings.php new file mode 100644 index 0000000..a6f9caa --- /dev/null +++ b/tests/settings.php @@ -0,0 +1,4 @@ +