diff --git a/src/auth/PhutilGoogleAuthAdapter.php b/src/auth/PhutilGoogleAuthAdapter.php index 8462730..055a743 100644 --- a/src/auth/PhutilGoogleAuthAdapter.php +++ b/src/auth/PhutilGoogleAuthAdapter.php @@ -1,170 +1,170 @@ getOAuthAccountData('emails', array()); foreach ($emails as $email) { if (idx($email, 'type') == 'account') { return idx($email, 'value'); } } throw new Exception( pht( - 'Expected to retrieve an "account" email from Google Plus API call ', + 'Expected to retrieve an "account" email from Google Plus API call '. 'to identify account, but failed.')); } public function getAccountEmail() { return $this->getAccountID(); } public function getAccountName() { // Guess account name from email address, this is just a hint anyway. $email = $this->getAccountEmail(); $email = explode('@', $email); $email = head($email); return $email; } public function getAccountImageURI() { return $this->getOAuthAccountData('picture'); } public function getAccountURI() { return 'https://plus.google.com/'.$this->getOAuthAccountData('id'); } public function getAccountRealName() { $name = $this->getOAuthAccountData('name', array()); // TODO: This could probably be made cleaner by looking up the API, but // this should work to unbreak logins. $parts = array(); $parts[] = idx($name, 'givenName'); unset($name['givenName']); $parts[] = idx($name, 'familyName'); unset($name['familyName']); $parts = array_merge($parts, $name); $parts = array_filter($parts); return implode(' ', $parts); } protected function getAuthenticateBaseURI() { return 'https://accounts.google.com/o/oauth2/auth'; } protected function getTokenBaseURI() { return 'https://accounts.google.com/o/oauth2/token'; } public function getScope() { $scopes = array( 'email', 'profile', ); return implode(' ', $scopes); } public function getExtraAuthenticateParameters() { return array( 'response_type' => 'code', ); } public function getExtraTokenParameters() { return array( 'grant_type' => 'authorization_code', ); } protected function loadOAuthAccountData() { $uri = new PhutilURI('https://www.googleapis.com/plus/v1/people/me'); $uri->setQueryParam('access_token', $this->getAccessToken()); $future = new HTTPSFuture($uri); list($status, $body) = $future->resolve(); if ($status->isError()) { $this->tryToThrowSpecializedError($status, $body); throw $status; } $data = json_decode($body, true); if (!is_array($data)) { throw new Exception( 'Expected valid JSON response from Google account data request, '. 'got: '.$body); } return $data; } private function tryToThrowSpecializedError($status, $raw_body) { if (!($status instanceof HTTPFutureHTTPResponseStatus)) { return; } if ($status->getStatusCode() != 403) { return; } $body = phutil_json_decode($raw_body); if (!$body) { return; } if (empty($body['error']['errors'][0])) { return; } $error = $body['error']['errors'][0]; $domain = idx($error, 'domain'); $reason = idx($error, 'reason'); if ($domain == 'usageLimits' && $reason == 'accessNotConfigured') { throw new PhutilAuthConfigurationException( pht( 'Google returned an "accessNotConfigured" error. This usually means '. 'you need to enable the "Google+ API" in your Google Cloud Console, '. 'under "APIs".'. "\n\n". 'Around March 2014, Google made some API changes which require this '. 'configuration adjustment.'. "\n\n". 'Normally, you can resolve this issue by going to %s, then '. 'clicking "API Project", then "APIs & auth", then turning the '. '"Google+ API" on. The names you see on the console may be '. 'different depending on how your integration is set up. If you '. 'are not sure, you can hunt through the projects until you find '. 'the one associated with the right Application ID under '. '"Credentials". The Application ID this install is using is "%s".'. "\n\n". '(If you are unable to log into Phabricator, you can use '. '"bin/auth recover" to recover access to an administrator '. 'account.)'. "\n\n". 'Full HTTP Response'. "\n\n%s", 'https://console.developers.google.com/', $this->getClientID(), $raw_body)); } } } diff --git a/src/conduit/ConduitClient.php b/src/conduit/ConduitClient.php index 5364431..0fbd50b 100644 --- a/src/conduit/ConduitClient.php +++ b/src/conduit/ConduitClient.php @@ -1,349 +1,349 @@ connectionID; } public function __construct($uri) { $this->uri = new PhutilURI($uri); if (!strlen($this->uri->getDomain())) { throw new Exception("Conduit URI '{$uri}' must include a valid host."); } $this->host = $this->uri->getDomain(); } /** * Override the domain specified in the service URI and provide a specific * host identity. * * This can be used to connect to a specific node in a cluster environment. */ public function setHost($host) { $this->host = $host; return $this; } public function getHost() { return $this->host; } public function setConduitToken($conduit_token) { $this->conduitToken = $conduit_token; return $this; } public function getConduitToken() { return $this->conduitToken; } public function callMethodSynchronous($method, array $params) { return $this->callMethod($method, $params)->resolve(); } public function didReceiveResponse($method, $data) { if ($method == 'conduit.connect') { $this->sessionKey = idx($data, 'sessionKey'); $this->connectionID = idx($data, 'connectionID'); } return $data; } public function setTimeout($timeout) { $this->timeout = $timeout; return $this; } public function setSigningKeys( $public_key, PhutilOpaqueEnvelope $private_key) { $this->publicKey = $public_key; $this->privateKey = $private_key; return $this; } public function callMethod($method, array $params) { $meta = array(); if ($this->sessionKey) { $meta['sessionKey'] = $this->sessionKey; } if ($this->connectionID) { $meta['connectionID'] = $this->connectionID; } if ($method == 'conduit.connect') { $certificate = idx($params, 'certificate'); if ($certificate) { $token = time(); $params['authToken'] = $token; $params['authSignature'] = sha1($token.$certificate); } unset($params['certificate']); } if ($this->privateKey && $this->publicKey) { $meta['auth.type'] = self::AUTH_ASYMMETRIC; $meta['auth.key'] = $this->publicKey; $meta['auth.host'] = $this->getHostString(); $signature = $this->signRequest($method, $params, $meta); $meta['auth.signature'] = $signature; } if ($this->conduitToken) { $meta['token'] = $this->conduitToken; } if ($meta) { $params['__conduit__'] = $meta; } $uri = id(clone $this->uri)->setPath('/api/'.$method); $data = array( 'params' => json_encode($params), 'output' => 'json', // This is a hint to Phabricator that the client expects a Conduit // response. It is not necessary, but provides better error messages in // some cases. '__conduit__' => true, ); // Always use the cURL-based HTTPSFuture, for proxy support and other // protocol edge cases that HTTPFuture does not support. $core_future = new HTTPSFuture($uri, $data); $core_future->addHeader('Host', $this->getHost()); $core_future->setMethod('POST'); $core_future->setTimeout($this->timeout); if ($this->username !== null) { $core_future->setHTTPBasicAuthCredentials( $this->username, $this->password); } $conduit_future = new ConduitFuture($core_future); $conduit_future->setClient($this, $method); $conduit_future->beginProfile($data); $conduit_future->isReady(); return $conduit_future; } public function setBasicAuthCredentials($username, $password) { $this->username = $username; $this->password = new PhutilOpaqueEnvelope($password); return $this; } private function getHostString() { $host = $this->getHost(); $uri = new PhutilURI($this->uri); $port = $uri->getPort(); if (!$port) { switch ($uri->getProtocol()) { case 'https': $port = 443; break; default: $port = 80; break; } } return $host.':'.$port; } private function signRequest( $method, array $params, array $meta) { $input = self::encodeRequestDataForSignature( $method, $params, $meta); $signature = null; $result = openssl_sign( $input, $signature, $this->privateKey->openEnvelope()); if (!$result) { throw new Exception('Unable to sign Conduit request with signing key.'); } return self::SIGNATURE_CONSIGN_1.base64_encode($signature); } public static function verifySignature( $method, array $params, array $meta, $openssl_public_key) { $auth_type = idx($meta, 'auth.type'); switch ($auth_type) { case self::AUTH_ASYMMETRIC: break; default: throw new Exception( pht( 'Unable to verify request signature, specified "auth.type" '. '("%s") is unknown.', $auth_type)); } $public_key = idx($meta, 'auth.key'); if (!strlen($public_key)) { throw new Exception( pht( 'Unable to verify request signature, no "auth.key" present in '. 'request protocol information.')); } $signature = idx($meta, 'auth.signature'); if (!strlen($signature)) { throw new Exception( pht( 'Unable to verify request signature, no "auth.signature" present '. 'in request protocol information.')); } $prefix = self::SIGNATURE_CONSIGN_1; if (strncmp($signature, $prefix, strlen($prefix)) !== 0) { throw new Exception( pht( 'Unable to verify request signature, signature format is not '. 'known.')); } $signature = substr($signature, strlen($prefix)); $input = self::encodeRequestDataForSignature( $method, $params, $meta); $signature = base64_decode($signature); $trap = new PhutilErrorTrap(); $result = @openssl_verify( $input, $signature, $openssl_public_key); $err = $trap->getErrorsAsString(); $trap->destroy(); if ($result === 1) { // Signature is good. return true; } else if ($result === 0) { // Signature is bad. throw new Exception( pht( 'Request signature verification failed: signature is not correct.')); } else { // Some kind of error. if (strlen($err)) { throw new Exception( pht( 'OpenSSL encountered an error verifying the request signature: '. '%s', $err)); } else { throw new Exception( pht( - 'OpenSSL encountered an unknown error verifying the request.', + 'OpenSSL encountered an unknown error verifying the request: %s', $err)); } } } private static function encodeRequestDataForSignature( $method, array $params, array $meta) { unset($meta['auth.signature']); $structure = array( 'method' => $method, 'protocol' => $meta, 'parameters' => $params, ); return self::encodeRawDataForSignature($structure); } public static function encodeRawDataForSignature($data) { $out = array(); if (is_array($data)) { if (!$data || (array_keys($data) == range(0, count($data) - 1))) { $out[] = 'A'; $out[] = count($data); $out[] = ':'; foreach ($data as $value) { $out[] = self::encodeRawDataForSignature($value); } } else { ksort($data); $out[] = 'O'; $out[] = count($data); $out[] = ':'; foreach ($data as $key => $value) { $out[] = self::encodeRawDataForSignature($key); $out[] = self::encodeRawDataForSignature($value); } } } else if (is_string($data)) { $out[] = 'S'; $out[] = strlen($data); $out[] = ':'; $out[] = $data; } else if (is_integer($data)) { $out[] = 'I'; $out[] = strlen((string)$data); $out[] = ':'; $out[] = (string)$data; } else if (is_null($data)) { $out[] = 'N'; $out[] = ':'; } else if ($data === true) { $out[] = 'B1:'; } else if ($data === false) { $out[] = 'B0:'; } else { throw new Exception( pht( 'Unexpected data type in request data: %s.', gettype($data))); } return implode('', $out); } } diff --git a/src/parser/xhpast/__tests__/PHPASTParserTestCase.php b/src/parser/xhpast/__tests__/PHPASTParserTestCase.php index bf7e110..c6a0f4f 100644 --- a/src/parser/xhpast/__tests__/PHPASTParserTestCase.php +++ b/src/parser/xhpast/__tests__/PHPASTParserTestCase.php @@ -1,140 +1,140 @@ assertSkipped(pht('xhpast is not built or not up to date.')); } } $dir = dirname(__FILE__).'/data/'; foreach (Filesystem::listDirectory($dir) as $file) { if (preg_match('/\.test$/', $file)) { $this->executeParserTest($file, Filesystem::readFile($dir.$file)); } } } private function executeParserTest($name, $data) { $data = explode("\n", $data, 2); if (count($data) !== 2) { throw new Exception( pht('Expected multiple lines in parser test file "%s".', $name)); } $head = head($data); $body = last($data); if (!preg_match('/^#/', $head)) { throw new Exception( pht( 'Expected first line of parser test file "%s" to begin with "#" '. 'and specify test options.', $name)); } $head = preg_replace('/^#\s*/', '', $head); $options_parser = new PhutilSimpleOptions(); $options = $options_parser->parse($head); $type = null; foreach ($options as $key => $value) { switch ($key) { case 'pass': case 'fail-syntax': case 'fail-parse': if ($type !== null) { throw new Exception( pht( - 'Test file "%s" unexpectedly specifies multiple expected ', + 'Test file "%s" unexpectedly specifies multiple expected '. 'test outcomes.', $name)); } $type = $key; break; case 'comment': // Human readable comment providing test case information. break; case 'rtrim': // Allows construction of tests which rely on EOF without newlines. $body = rtrim($body); break; default: throw new Exception( pht( 'Test file "%s" has unknown option "%s" in its options '. 'string.', $name, $key)); } } if ($type === null) { throw new Exception( pht( 'Test file "%s" does not specify a test result (like "pass") in '. 'its options string.', $name)); } $future = PhutilXHPASTBinary::getParserFuture($body); list($err, $stdout, $stderr) = $future->resolve(); switch ($type) { case 'pass': case 'fail-parse': $this->assertEqual(0, $err, pht('Exit code for "%s".', $name)); $expect_name = preg_replace('/\.test$/', '.expect', $name); $dir = dirname(__FILE__).'/data/'; $expect = Filesystem::readFile($dir.$expect_name); $expect = json_decode($expect, true); if (!is_array($expect)) { throw new Exception( pht( 'Test ".expect" file "%s" for test "%s" is not valid JSON.', $expect_name, $name)); } $stdout = json_decode($stdout, true); if (!is_array($stdout)) { throw new Exception( pht( 'Output for test file "%s" is not valid JSON.', $name)); } $json = new PhutilJSON(); $expect_nice = $json->encodeFormatted($expect); $stdout_nice = $json->encodeFormatted($stdout); if ($type == 'pass') { $this->assertEqual( $expect_nice, $stdout_nice, pht('Parser output for "%s".', $name)); } else { $this->assertFalse( ($expect_nice == $stdout_nice), pht('Expected parser to parse "%s" incorrectly.', $name)); } break; case 'fail-syntax': $this->assertEqual(1, $err, pht('Exit code for "%s".', $name)); $this->assertTrue( (bool)preg_match('/syntax error/', $stderr), pht('Expect "syntax error" in stderr or "%s".', $name)); break; } } }