Changeset View
Changeset View
Standalone View
Standalone View
src/conduit/ConduitClient.php
| <?php | <?php | ||||
| final class ConduitClient { | final class ConduitClient { | ||||
| private $uri; | private $uri; | ||||
| private $connectionID; | private $connectionID; | ||||
| private $sessionKey; | private $sessionKey; | ||||
| private $timeout = 300.0; | private $timeout = 300.0; | ||||
| private $username; | private $username; | ||||
| private $password; | private $password; | ||||
| private $publicKey; | |||||
| private $privateKey; | |||||
| const AUTH_ASYMMETRIC = 'asymmetric'; | |||||
| const SIGNATURE_CONSIGN_1 = 'Consign1.0/'; | |||||
| public function getConnectionID() { | public function getConnectionID() { | ||||
| return $this->connectionID; | return $this->connectionID; | ||||
| } | } | ||||
| public function __construct($uri) { | public function __construct($uri) { | ||||
| $this->uri = new PhutilURI($uri); | $this->uri = new PhutilURI($uri); | ||||
| if (!strlen($this->uri->getDomain())) { | if (!strlen($this->uri->getDomain())) { | ||||
| Show All 13 Lines | public function didReceiveResponse($method, $data) { | ||||
| return $data; | return $data; | ||||
| } | } | ||||
| public function setTimeout($timeout) { | public function setTimeout($timeout) { | ||||
| $this->timeout = $timeout; | $this->timeout = $timeout; | ||||
| return $this; | 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) { | public function callMethod($method, array $params) { | ||||
| $meta = array(); | $meta = array(); | ||||
| if ($this->sessionKey) { | if ($this->sessionKey) { | ||||
| $meta['sessionKey'] = $this->sessionKey; | $meta['sessionKey'] = $this->sessionKey; | ||||
| } | } | ||||
| if ($this->connectionID) { | if ($this->connectionID) { | ||||
| $meta['connectionID'] = $this->connectionID; | $meta['connectionID'] = $this->connectionID; | ||||
| } | } | ||||
| if ($method == 'conduit.connect') { | if ($method == 'conduit.connect') { | ||||
| $certificate = idx($params, 'certificate'); | $certificate = idx($params, 'certificate'); | ||||
| if ($certificate) { | if ($certificate) { | ||||
| $token = time(); | $token = time(); | ||||
| $params['authToken'] = $token; | $params['authToken'] = $token; | ||||
| $params['authSignature'] = sha1($token.$certificate); | $params['authSignature'] = sha1($token.$certificate); | ||||
| } | } | ||||
| unset($params['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 ($meta) { | if ($meta) { | ||||
| $params['__conduit__'] = $meta; | $params['__conduit__'] = $meta; | ||||
| } | } | ||||
| $uri = id(clone $this->uri)->setPath('/api/'.$method); | $uri = id(clone $this->uri)->setPath('/api/'.$method); | ||||
| $data = array( | $data = array( | ||||
| 'params' => json_encode($params), | 'params' => json_encode($params), | ||||
| Show All 27 Lines | final class ConduitClient { | ||||
| } | } | ||||
| public function setBasicAuthCredentials($username, $password) { | public function setBasicAuthCredentials($username, $password) { | ||||
| $this->username = $username; | $this->username = $username; | ||||
| $this->password = new PhutilOpaqueEnvelope($password); | $this->password = new PhutilOpaqueEnvelope($password); | ||||
| return $this; | return $this; | ||||
| } | } | ||||
| private function getHostString() { | |||||
| $uri = new PhutilURI($this->uri); | |||||
| $host = $uri->getDomain(); | |||||
| $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.', | |||||
| $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); | |||||
| } | |||||
| } | } | ||||