From 773f125820e4709a0c1a28a212423369416b4daa Mon Sep 17 00:00:00 2001 From: David Foster Date: Tue, 19 Jun 2012 15:53:04 -0700 Subject: [PATCH 1/4] Authentication. Document supported PHP version as 5.2. --- README.md | 2 +- Splunk.php | 30 ++++++++ Splunk/Context.php | 96 ++++++++++++++++++++++++++ Splunk/Http.php | 156 ++++++++++++++++++++++++++++++++++++++++++ Splunk/Utils.php | 48 +++++++++++++ tests/ContextTest.php | 76 ++++++++++++++++++++ tests/HttpTest.php | 30 ++++++++ tests/Makefile | 2 + tests/README.md | 13 ++++ 9 files changed, 452 insertions(+), 1 deletion(-) create mode 100644 Splunk.php create mode 100644 Splunk/Context.php create mode 100644 Splunk/Http.php create mode 100644 Splunk/Utils.php create mode 100644 tests/ContextTest.php create mode 100644 tests/HttpTest.php create mode 100644 tests/Makefile create mode 100644 tests/README.md 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_Utils::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..5299731 --- /dev/null +++ b/Splunk/Http.php @@ -0,0 +1,156 @@ +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_Utils::getTextContentAtXpath( + new SimpleXMLElement($response['body']), + '/response/messages/msg'); + } + + // === Accessors === + + public function getResponse() + { + return $this->response; + } +} diff --git a/Splunk/Utils.php b/Splunk/Utils.php new file mode 100644 index 0000000..0243d81 --- /dev/null +++ b/Splunk/Utils.php @@ -0,0 +1,48 @@ +xpath($xpathExpr); + return (count($matchingElements) == 0) + ? NULL + : Splunk_Utils::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..43044af --- /dev/null +++ b/tests/ContextTest.php @@ -0,0 +1,76 @@ + 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(); + } +} 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 From a81e8083b45a4249d7a20cf46a04931064c32ed1 Mon Sep 17 00:00:00 2001 From: David Foster Date: Tue, 19 Jun 2012 15:57:38 -0700 Subject: [PATCH 2/4] Rename Utils to Util. --- Splunk/Context.php | 4 +--- Splunk/Http.php | 4 +--- Splunk/{Utils.php => Util.php} | 4 ++-- 3 files changed, 4 insertions(+), 8 deletions(-) rename Splunk/{Utils.php => Util.php} (93%) diff --git a/Splunk/Context.php b/Splunk/Context.php index 6fec7b1..4b6e0b1 100644 --- a/Splunk/Context.php +++ b/Splunk/Context.php @@ -15,8 +15,6 @@ * under the License. */ -require_once 'Utils.php'; - class Splunk_Context { private $username; @@ -69,7 +67,7 @@ public function login() 'password' => $this->password, )); - $sessionKey = Splunk_Utils::getTextContentAtXpath( + $sessionKey = Splunk_Util::getTextContentAtXpath( new SimpleXMLElement($response['body']), '/response/sessionKey'); diff --git a/Splunk/Http.php b/Splunk/Http.php index 5299731..182c060 100644 --- a/Splunk/Http.php +++ b/Splunk/Http.php @@ -15,8 +15,6 @@ * under the License. */ -require_once 'Utils.php'; - /** * HTTP abstraction layer. * @@ -142,7 +140,7 @@ public function __construct($response) private static function parseFirstMessageFrom($response) { - return Splunk_Utils::getTextContentAtXpath( + return Splunk_Util::getTextContentAtXpath( new SimpleXMLElement($response['body']), '/response/messages/msg'); } diff --git a/Splunk/Utils.php b/Splunk/Util.php similarity index 93% rename from Splunk/Utils.php rename to Splunk/Util.php index 0243d81..afdac71 100644 --- a/Splunk/Utils.php +++ b/Splunk/Util.php @@ -20,7 +20,7 @@ * * @package Splunk */ -class Splunk_Utils +class Splunk_Util { /** * @param SimpleXMLElement $xmlElement @@ -32,7 +32,7 @@ public static function getTextContentAtXpath($xmlElement, $xpathExpr) $matchingElements = $xmlElement->xpath($xpathExpr); return (count($matchingElements) == 0) ? NULL - : Splunk_Utils::getTextContentOfXmlElement($matchingElements[0]); + : Splunk_Util::getTextContentOfXmlElement($matchingElements[0]); } /** From 14b0057a571ad0dace1c3fce4c448ac246be0cfb Mon Sep 17 00:00:00 2001 From: David Foster Date: Tue, 19 Jun 2012 16:29:31 -0700 Subject: [PATCH 3/4] Documentation fixes. --- .gitignore | 3 +++ Makefile | 4 ++++ Splunk/Context.php | 7 ++++++- 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 Makefile diff --git a/.gitignore b/.gitignore index fd29596..a820421 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ # OS X .DS_Store + +# Generated documentation +docs/ 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/Splunk/Context.php b/Splunk/Context.php index 4b6e0b1..c554093 100644 --- a/Splunk/Context.php +++ b/Splunk/Context.php @@ -15,6 +15,11 @@ * under the License. */ +/** + * This class allows clients to issue HTTP requests to a Splunk server. + * + * @package Splunk + */ class Splunk_Context { private $username; @@ -27,7 +32,7 @@ class Splunk_Context private $token; /** - * @param array $args (optional) { + * @param array $args { * 'username' => (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". From 4f7bd72c66fe5c048cc0eef424217dd250bc5fc4 Mon Sep 17 00:00:00 2001 From: David Foster Date: Wed, 20 Jun 2012 11:59:27 -0700 Subject: [PATCH 4/4] Add first test that connects to a real Splunk server. --- .gitignore | 3 +++ tests/ContextTest.php | 9 +++++++++ tests/settings.default.php | 13 +++++++++++++ tests/settings.php | 4 ++++ 4 files changed, 29 insertions(+) create mode 100644 tests/settings.default.php create mode 100644 tests/settings.php diff --git a/.gitignore b/.gitignore index a820421..c099e65 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ # Generated documentation docs/ + +# Local settings +*.local.php diff --git a/tests/ContextTest.php b/tests/ContextTest.php index 43044af..4d71ccf 100644 --- a/tests/ContextTest.php +++ b/tests/ContextTest.php @@ -16,6 +16,7 @@ */ require_once '../Splunk.php'; +require_once 'settings.php'; class ContextTest extends PHPUnit_Framework_TestCase { @@ -73,4 +74,12 @@ public function testLoginFailDueToBadPassword() )); $context->login(); } + + public function testLoginSuccessOnRealServer() + { + global $Splunk_testSettings; + + $context = new Splunk_Context($Splunk_testSettings['connectArgs']); + $context->login(); + } } 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 @@ +