PageRenderTime 27ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/src/conduit/ConduitClient.php

http://github.com/facebook/libphutil
PHP | 363 lines | 290 code | 59 blank | 14 comment | 43 complexity | 8b3162c441584d2f7cd37d6a51fe86f4 MD5 | raw file
Possible License(s): Apache-2.0
  1. <?php
  2. final class ConduitClient extends Phobject {
  3. private $uri;
  4. private $host;
  5. private $connectionID;
  6. private $sessionKey;
  7. private $timeout = 300.0;
  8. private $username;
  9. private $password;
  10. private $publicKey;
  11. private $privateKey;
  12. private $conduitToken;
  13. private $oauthToken;
  14. const AUTH_ASYMMETRIC = 'asymmetric';
  15. const SIGNATURE_CONSIGN_1 = 'Consign1.0/';
  16. public function getConnectionID() {
  17. return $this->connectionID;
  18. }
  19. public function __construct($uri) {
  20. $this->uri = new PhutilURI($uri);
  21. if (!strlen($this->uri->getDomain())) {
  22. throw new Exception(
  23. pht("Conduit URI '%s' must include a valid host.", $uri));
  24. }
  25. $this->host = $this->uri->getDomain();
  26. }
  27. /**
  28. * Override the domain specified in the service URI and provide a specific
  29. * host identity.
  30. *
  31. * This can be used to connect to a specific node in a cluster environment.
  32. */
  33. public function setHost($host) {
  34. $this->host = $host;
  35. return $this;
  36. }
  37. public function getHost() {
  38. return $this->host;
  39. }
  40. public function setConduitToken($conduit_token) {
  41. $this->conduitToken = $conduit_token;
  42. return $this;
  43. }
  44. public function getConduitToken() {
  45. return $this->conduitToken;
  46. }
  47. public function setOAuthToken($oauth_token) {
  48. $this->oauthToken = $oauth_token;
  49. return $this;
  50. }
  51. public function callMethodSynchronous($method, array $params) {
  52. return $this->callMethod($method, $params)->resolve();
  53. }
  54. public function didReceiveResponse($method, $data) {
  55. if ($method == 'conduit.connect') {
  56. $this->sessionKey = idx($data, 'sessionKey');
  57. $this->connectionID = idx($data, 'connectionID');
  58. }
  59. return $data;
  60. }
  61. public function setTimeout($timeout) {
  62. $this->timeout = $timeout;
  63. return $this;
  64. }
  65. public function setSigningKeys(
  66. $public_key,
  67. PhutilOpaqueEnvelope $private_key) {
  68. $this->publicKey = $public_key;
  69. $this->privateKey = $private_key;
  70. return $this;
  71. }
  72. public function callMethod($method, array $params) {
  73. $meta = array();
  74. if ($this->sessionKey) {
  75. $meta['sessionKey'] = $this->sessionKey;
  76. }
  77. if ($this->connectionID) {
  78. $meta['connectionID'] = $this->connectionID;
  79. }
  80. if ($method == 'conduit.connect') {
  81. $certificate = idx($params, 'certificate');
  82. if ($certificate) {
  83. $token = time();
  84. $params['authToken'] = $token;
  85. $params['authSignature'] = sha1($token.$certificate);
  86. }
  87. unset($params['certificate']);
  88. }
  89. if ($this->privateKey && $this->publicKey) {
  90. $meta['auth.type'] = self::AUTH_ASYMMETRIC;
  91. $meta['auth.key'] = $this->publicKey;
  92. $meta['auth.host'] = $this->getHostString();
  93. $signature = $this->signRequest($method, $params, $meta);
  94. $meta['auth.signature'] = $signature;
  95. }
  96. if ($this->conduitToken) {
  97. $meta['token'] = $this->conduitToken;
  98. }
  99. if ($this->oauthToken) {
  100. $meta['access_token'] = $this->oauthToken;
  101. }
  102. if ($meta) {
  103. $params['__conduit__'] = $meta;
  104. }
  105. $uri = id(clone $this->uri)->setPath('/api/'.$method);
  106. $data = array(
  107. 'params' => json_encode($params),
  108. 'output' => 'json',
  109. // This is a hint to Phabricator that the client expects a Conduit
  110. // response. It is not necessary, but provides better error messages in
  111. // some cases.
  112. '__conduit__' => true,
  113. );
  114. // Always use the cURL-based HTTPSFuture, for proxy support and other
  115. // protocol edge cases that HTTPFuture does not support.
  116. $core_future = new HTTPSFuture($uri, $data);
  117. $core_future->addHeader('Host', $this->getHost());
  118. $core_future->setMethod('POST');
  119. $core_future->setTimeout($this->timeout);
  120. if ($this->username !== null) {
  121. $core_future->setHTTPBasicAuthCredentials(
  122. $this->username,
  123. $this->password);
  124. }
  125. $conduit_future = new ConduitFuture($core_future);
  126. $conduit_future->setClient($this, $method);
  127. $conduit_future->beginProfile($data);
  128. $conduit_future->isReady();
  129. return $conduit_future;
  130. }
  131. public function setBasicAuthCredentials($username, $password) {
  132. $this->username = $username;
  133. $this->password = new PhutilOpaqueEnvelope($password);
  134. return $this;
  135. }
  136. private function getHostString() {
  137. $host = $this->getHost();
  138. $uri = new PhutilURI($this->uri);
  139. $port = $uri->getPort();
  140. if (!$port) {
  141. switch ($uri->getProtocol()) {
  142. case 'https':
  143. $port = 443;
  144. break;
  145. default:
  146. $port = 80;
  147. break;
  148. }
  149. }
  150. return $host.':'.$port;
  151. }
  152. private function signRequest(
  153. $method,
  154. array $params,
  155. array $meta) {
  156. $input = self::encodeRequestDataForSignature(
  157. $method,
  158. $params,
  159. $meta);
  160. $signature = null;
  161. $result = openssl_sign(
  162. $input,
  163. $signature,
  164. $this->privateKey->openEnvelope());
  165. if (!$result) {
  166. throw new Exception(
  167. pht('Unable to sign Conduit request with signing key.'));
  168. }
  169. return self::SIGNATURE_CONSIGN_1.base64_encode($signature);
  170. }
  171. public static function verifySignature(
  172. $method,
  173. array $params,
  174. array $meta,
  175. $openssl_public_key) {
  176. $auth_type = idx($meta, 'auth.type');
  177. switch ($auth_type) {
  178. case self::AUTH_ASYMMETRIC:
  179. break;
  180. default:
  181. throw new Exception(
  182. pht(
  183. 'Unable to verify request signature, specified "%s" '.
  184. '("%s") is unknown.',
  185. 'auth.type',
  186. $auth_type));
  187. }
  188. $public_key = idx($meta, 'auth.key');
  189. if (!strlen($public_key)) {
  190. throw new Exception(
  191. pht(
  192. 'Unable to verify request signature, no "%s" present in '.
  193. 'request protocol information.',
  194. 'auth.key'));
  195. }
  196. $signature = idx($meta, 'auth.signature');
  197. if (!strlen($signature)) {
  198. throw new Exception(
  199. pht(
  200. 'Unable to verify request signature, no "%s" present '.
  201. 'in request protocol information.',
  202. 'auth.signature'));
  203. }
  204. $prefix = self::SIGNATURE_CONSIGN_1;
  205. if (strncmp($signature, $prefix, strlen($prefix)) !== 0) {
  206. throw new Exception(
  207. pht(
  208. 'Unable to verify request signature, signature format is not '.
  209. 'known.'));
  210. }
  211. $signature = substr($signature, strlen($prefix));
  212. $input = self::encodeRequestDataForSignature(
  213. $method,
  214. $params,
  215. $meta);
  216. $signature = base64_decode($signature);
  217. $trap = new PhutilErrorTrap();
  218. $result = @openssl_verify(
  219. $input,
  220. $signature,
  221. $openssl_public_key);
  222. $err = $trap->getErrorsAsString();
  223. $trap->destroy();
  224. if ($result === 1) {
  225. // Signature is good.
  226. return true;
  227. } else if ($result === 0) {
  228. // Signature is bad.
  229. throw new Exception(
  230. pht(
  231. 'Request signature verification failed: signature is not correct.'));
  232. } else {
  233. // Some kind of error.
  234. if (strlen($err)) {
  235. throw new Exception(
  236. pht(
  237. 'OpenSSL encountered an error verifying the request signature: %s',
  238. $err));
  239. } else {
  240. throw new Exception(
  241. pht(
  242. 'OpenSSL encountered an unknown error verifying the request: %s',
  243. $err));
  244. }
  245. }
  246. }
  247. private static function encodeRequestDataForSignature(
  248. $method,
  249. array $params,
  250. array $meta) {
  251. unset($meta['auth.signature']);
  252. $structure = array(
  253. 'method' => $method,
  254. 'protocol' => $meta,
  255. 'parameters' => $params,
  256. );
  257. return self::encodeRawDataForSignature($structure);
  258. }
  259. public static function encodeRawDataForSignature($data) {
  260. $out = array();
  261. if (is_array($data)) {
  262. if (!$data || (array_keys($data) == range(0, count($data) - 1))) {
  263. $out[] = 'A';
  264. $out[] = count($data);
  265. $out[] = ':';
  266. foreach ($data as $value) {
  267. $out[] = self::encodeRawDataForSignature($value);
  268. }
  269. } else {
  270. ksort($data);
  271. $out[] = 'O';
  272. $out[] = count($data);
  273. $out[] = ':';
  274. foreach ($data as $key => $value) {
  275. $out[] = self::encodeRawDataForSignature($key);
  276. $out[] = self::encodeRawDataForSignature($value);
  277. }
  278. }
  279. } else if (is_string($data)) {
  280. $out[] = 'S';
  281. $out[] = strlen($data);
  282. $out[] = ':';
  283. $out[] = $data;
  284. } else if (is_int($data)) {
  285. $out[] = 'I';
  286. $out[] = strlen((string)$data);
  287. $out[] = ':';
  288. $out[] = (string)$data;
  289. } else if (is_null($data)) {
  290. $out[] = 'N';
  291. $out[] = ':';
  292. } else if ($data === true) {
  293. $out[] = 'B1:';
  294. } else if ($data === false) {
  295. $out[] = 'B0:';
  296. } else {
  297. throw new Exception(
  298. pht(
  299. 'Unexpected data type in request data: %s.',
  300. gettype($data)));
  301. }
  302. return implode('', $out);
  303. }
  304. }