SwiftVirtualRESTService.php 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. <?php
  2. /**
  3. * Virtual HTTP service client for Swift
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. */
  22. /**
  23. * Example virtual rest service for OpenStack Swift
  24. * @todo caching support (APC/memcached)
  25. * @since 1.23
  26. */
  27. class SwiftVirtualRESTService extends VirtualRESTService {
  28. /** @var array */
  29. protected $authCreds;
  30. /** @var int UNIX timestamp */
  31. protected $authSessionTimestamp = 0;
  32. /** @var int UNIX timestamp */
  33. protected $authErrorTimestamp = null;
  34. /** @var int */
  35. protected $authCachedStatus = null;
  36. /** @var string */
  37. protected $authCachedReason = null;
  38. /**
  39. * @param array $params Key/value map
  40. * - swiftAuthUrl : Swift authentication server URL
  41. * - swiftUser : Swift user used by MediaWiki (account:username)
  42. * - swiftKey : Swift authentication key for the above user
  43. * - swiftAuthTTL : Swift authentication TTL (seconds)
  44. */
  45. public function __construct( array $params ) {
  46. // set up defaults and merge them with the given params
  47. $mparams = array_merge( [
  48. 'name' => 'swift'
  49. ], $params );
  50. parent::__construct( $mparams );
  51. }
  52. /**
  53. * @return int|bool HTTP status on cached failure
  54. */
  55. protected function needsAuthRequest() {
  56. if ( !$this->authCreds ) {
  57. return true;
  58. }
  59. if ( $this->authErrorTimestamp !== null ) {
  60. if ( ( time() - $this->authErrorTimestamp ) < 60 ) {
  61. return $this->authCachedStatus; // failed last attempt; don't bother
  62. } else { // actually retry this time
  63. $this->authErrorTimestamp = null;
  64. }
  65. }
  66. // Session keys expire after a while, so we renew them periodically
  67. return ( ( time() - $this->authSessionTimestamp ) > $this->params['swiftAuthTTL'] );
  68. }
  69. protected function applyAuthResponse( array $req ) {
  70. $this->authSessionTimestamp = 0;
  71. list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $req['response'];
  72. if ( $rcode >= 200 && $rcode <= 299 ) { // OK
  73. $this->authCreds = [
  74. 'auth_token' => $rhdrs['x-auth-token'],
  75. 'storage_url' => $rhdrs['x-storage-url']
  76. ];
  77. $this->authSessionTimestamp = time();
  78. return true;
  79. } elseif ( $rcode === 403 ) {
  80. $this->authCachedStatus = 401;
  81. $this->authCachedReason = 'Authorization Required';
  82. $this->authErrorTimestamp = time();
  83. return false;
  84. } else {
  85. $this->authCachedStatus = $rcode;
  86. $this->authCachedReason = $rdesc;
  87. $this->authErrorTimestamp = time();
  88. return null;
  89. }
  90. }
  91. public function onRequests( array $reqs, Closure $idGeneratorFunc ) {
  92. $result = [];
  93. $firstReq = reset( $reqs );
  94. if ( $firstReq && count( $reqs ) == 1 && isset( $firstReq['isAuth'] ) ) {
  95. // This was an authentication request for work requests...
  96. $result = $reqs; // no change
  97. } else {
  98. // These are actual work requests...
  99. $needsAuth = $this->needsAuthRequest();
  100. if ( $needsAuth === true ) {
  101. // These are work requests and we don't have any token to use.
  102. // Replace the work requests with an authentication request.
  103. $result = [
  104. $idGeneratorFunc() => [
  105. 'method' => 'GET',
  106. 'url' => $this->params['swiftAuthUrl'] . "/v1.0",
  107. 'headers' => [
  108. 'x-auth-user' => $this->params['swiftUser'],
  109. 'x-auth-key' => $this->params['swiftKey'] ],
  110. 'isAuth' => true,
  111. 'chain' => $reqs
  112. ]
  113. ];
  114. } elseif ( $needsAuth !== false ) {
  115. // These are work requests and authentication has previously failed.
  116. // It is most efficient to just give failed pseudo responses back for
  117. // the original work requests.
  118. foreach ( $reqs as $key => $req ) {
  119. $req['response'] = [
  120. 'code' => $this->authCachedStatus,
  121. 'reason' => $this->authCachedReason,
  122. 'headers' => [],
  123. 'body' => '',
  124. 'error' => ''
  125. ];
  126. $result[$key] = $req;
  127. }
  128. } else {
  129. // These are work requests and we have a token already.
  130. // Go through and mangle each request to include a token.
  131. foreach ( $reqs as $key => $req ) {
  132. // The default encoding treats the URL as a REST style path that uses
  133. // forward slash as a hierarchical delimiter (and never otherwise).
  134. // Subclasses can override this, and should be documented in any case.
  135. $parts = array_map( 'rawurlencode', explode( '/', $req['url'] ) );
  136. $req['url'] = $this->authCreds['storage_url'] . '/' . implode( '/', $parts );
  137. $req['headers']['x-auth-token'] = $this->authCreds['auth_token'];
  138. $result[$key] = $req;
  139. // @TODO: add ETag/Content-Length and such as needed
  140. }
  141. }
  142. }
  143. return $result;
  144. }
  145. public function onResponses( array $reqs, Closure $idGeneratorFunc ) {
  146. $firstReq = reset( $reqs );
  147. if ( $firstReq && count( $reqs ) == 1 && isset( $firstReq['isAuth'] ) ) {
  148. $result = [];
  149. // This was an authentication request for work requests...
  150. if ( $this->applyAuthResponse( $firstReq ) ) {
  151. // If it succeeded, we can subsitute the work requests back.
  152. // Call this recursively in order to munge and add headers.
  153. $result = $this->onRequests( $firstReq['chain'], $idGeneratorFunc );
  154. } else {
  155. // If it failed, it is most efficient to just give failing
  156. // pseudo-responses back for the actual work requests.
  157. foreach ( $firstReq['chain'] as $key => $req ) {
  158. $req['response'] = [
  159. 'code' => $this->authCachedStatus,
  160. 'reason' => $this->authCachedReason,
  161. 'headers' => [],
  162. 'body' => '',
  163. 'error' => ''
  164. ];
  165. $result[$key] = $req;
  166. }
  167. }
  168. } else {
  169. $result = $reqs; // no change
  170. }
  171. return $result;
  172. }
  173. }