Dotclear

source: plugins/pixearch/inc/OAuth.php @ 656

Revision 656, 21.8 KB checked in by hadrien, 14 years ago (diff)

Ajout du plugin Pixearch

Line 
1<?php
2// vim: foldmethod=marker
3
4/* Generic exception class
5 */
6class OAuthException extends Exception {/*{{{*/
7  // pass
8}/*}}}*/
9
10class OAuthConsumer {/*{{{*/
11  public $key;
12  public $secret;
13
14  function __construct($key, $secret, $callback_url=NULL) {/*{{{*/
15    $this->key = $key;
16    $this->secret = $secret;
17    $this->callback_url = $callback_url;
18  }/*}}}*/
19}/*}}}*/
20
21class OAuthToken {/*{{{*/
22  // access tokens and request tokens
23  public $key;
24  public $secret;
25
26  /**
27   * key = the token
28   * secret = the token secret
29   */
30  function __construct($key, $secret) {/*{{{*/
31    $this->key = $key;
32    $this->secret = $secret;
33  }/*}}}*/
34
35  /**
36   * generates the basic string serialization of a token that a server
37   * would respond to request_token and access_token calls with
38   */
39  function to_string() {/*{{{*/
40    return "oauth_token=" . OAuthUtil::urlencodeRFC3986($this->key) . 
41        "&oauth_token_secret=" . OAuthUtil::urlencodeRFC3986($this->secret);
42  }/*}}}*/
43
44  function __toString() {/*{{{*/
45    return $this->to_string();
46  }/*}}}*/
47}/*}}}*/
48
49class OAuthSignatureMethod {/*{{{*/
50  public function check_signature(&$request, $consumer, $token, $signature) {
51    $built = $this->build_signature($request, $consumer, $token);
52    return $built == $signature;
53  }
54}/*}}}*/
55
56class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod {/*{{{*/
57  function get_name() {/*{{{*/
58    return "HMAC-SHA1";
59  }/*}}}*/
60
61  public function build_signature($request, $consumer, $token) {/*{{{*/
62    $base_string = $request->get_signature_base_string();
63    $request->base_string = $base_string;
64
65    $key_parts = array(
66      $consumer->secret,
67      ($token) ? $token->secret : ""
68    );
69
70    $key_parts = array_map(array('OAuthUtil','urlencodeRFC3986'), $key_parts);
71    $key = implode('&', $key_parts);
72
73    return base64_encode( hash_hmac('sha1', $base_string, $key, true));
74  }/*}}}*/
75}/*}}}*/
76
77class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod {/*{{{*/
78  public function get_name() {/*{{{*/
79    return "PLAINTEXT";
80  }/*}}}*/
81
82  public function build_signature($request, $consumer, $token) {/*{{{*/
83    $sig = array(
84      OAuthUtil::urlencodeRFC3986($consumer->secret)
85    );
86
87    if ($token) {
88      array_push($sig, OAuthUtil::urlencodeRFC3986($token->secret));
89    } else {
90      array_push($sig, '');
91    }
92
93    $raw = implode("&", $sig);
94    // for debug purposes
95    $request->base_string = $raw;
96
97    return OAuthUtil::urlencodeRFC3986($raw);
98  }/*}}}*/
99}/*}}}*/
100
101class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod {/*{{{*/
102  public function get_name() {/*{{{*/
103    return "RSA-SHA1";
104  }/*}}}*/
105
106  protected function fetch_public_cert(&$request) {/*{{{*/
107    // not implemented yet, ideas are:
108    // (1) do a lookup in a table of trusted certs keyed off of consumer
109    // (2) fetch via http using a url provided by the requester
110    // (3) some sort of specific discovery code based on request
111    //
112    // either way should return a string representation of the certificate
113    throw Exception("fetch_public_cert not implemented");
114  }/*}}}*/
115
116  protected function fetch_private_cert(&$request) {/*{{{*/
117    // not implemented yet, ideas are:
118    // (1) do a lookup in a table of trusted certs keyed off of consumer
119    //
120    // either way should return a string representation of the certificate
121    throw Exception("fetch_private_cert not implemented");
122  }/*}}}*/
123
124  public function build_signature(&$request, $consumer, $token) {/*{{{*/
125    $base_string = $request->get_signature_base_string();
126    $request->base_string = $base_string;
127 
128    // Fetch the private key cert based on the request
129    $cert = $this->fetch_private_cert($request);
130
131    // Pull the private key ID from the certificate
132    $privatekeyid = openssl_get_privatekey($cert);
133
134    // Sign using the key
135    $ok = openssl_sign($base_string, $signature, $privatekeyid);   
136
137    // Release the key resource
138    openssl_free_key($privatekeyid);
139 
140    return base64_encode($signature);
141  } /*}}}*/
142
143  public function check_signature(&$request, $consumer, $token, $signature) {/*{{{*/
144    $decoded_sig = base64_decode($signature);
145
146    $base_string = $request->get_signature_base_string();
147 
148    // Fetch the public key cert based on the request
149    $cert = $this->fetch_public_cert($request);
150
151    // Pull the public key ID from the certificate
152    $publickeyid = openssl_get_publickey($cert);
153
154    // Check the computed signature against the one passed in the query
155    $ok = openssl_verify($base_string, $decoded_sig, $publickeyid);   
156
157    // Release the key resource
158    openssl_free_key($publickeyid);
159 
160    return $ok == 1;
161  } /*}}}*/
162}/*}}}*/
163
164class OAuthRequest {/*{{{*/
165  private $parameters;
166  private $http_method;
167  private $http_url;
168  // for debug purposes
169  public $base_string;
170  public static $version = '1.0';
171
172  function __construct($http_method, $http_url, $parameters=NULL) {/*{{{*/
173    @$parameters or $parameters = array();
174    $this->parameters = $parameters;
175    $this->http_method = $http_method;
176    $this->http_url = $http_url;
177  }/*}}}*/
178
179
180  /**
181   * attempt to build up a request from what was passed to the server
182   */
183  public static function from_request($http_method=NULL, $http_url=NULL, $parameters=NULL) {/*{{{*/
184    $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on") ? 'http' : 'https';
185    @$http_url or $http_url = $scheme . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
186    @$http_method or $http_method = $_SERVER['REQUEST_METHOD'];
187   
188    $request_headers = OAuthRequest::get_headers();
189
190    // let the library user override things however they'd like, if they know
191    // which parameters to use then go for it, for example XMLRPC might want to
192    // do this
193    if ($parameters) {
194      $req = new OAuthRequest($http_method, $http_url, $parameters);
195    }
196    // next check for the auth header, we need to do some extra stuff
197    // if that is the case, namely suck in the parameters from GET or POST
198    // so that we can include them in the signature
199    else if (@substr($request_headers['Authorization'], 0, 5) == "OAuth") {
200      $header_parameters = OAuthRequest::split_header($request_headers['Authorization']);
201      if ($http_method == "GET") {
202        $req_parameters = $_GET;
203      } 
204      else if ($http_method == "POST") {
205        $req_parameters = $_POST;
206      } 
207      $parameters = array_merge($header_parameters, $req_parameters);
208      $req = new OAuthRequest($http_method, $http_url, $parameters);
209    }
210    else if ($http_method == "GET") {
211      $req = new OAuthRequest($http_method, $http_url, $_GET);
212    }
213    else if ($http_method == "POST") {
214      $req = new OAuthRequest($http_method, $http_url, $_POST);
215    }
216    return $req;
217  }/*}}}*/
218
219  /**
220   * pretty much a helper function to set up the request
221   */
222  public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=array()) {/*{{{*/
223    @$parameters or $parameters = array();
224    $defaults = array("oauth_version" => OAuthRequest::$version,
225                      "oauth_nonce" => OAuthRequest::generate_nonce(),
226                      "oauth_timestamp" => OAuthRequest::generate_timestamp(),
227                      "oauth_consumer_key" => $consumer->key);
228    $parameters = array_merge($defaults, $parameters);
229
230    if ($token) {
231      $parameters['oauth_token'] = $token->key;
232    }
233    return new OAuthRequest($http_method, $http_url, $parameters);
234  }/*}}}*/
235
236  public function set_parameter($name, $value) {/*{{{*/
237    $this->parameters[$name] = $value;
238  }/*}}}*/
239
240  public function get_parameter($name) {/*{{{*/
241    return $this->parameters[$name];
242  }/*}}}*/
243
244  public function get_parameters() {/*{{{*/
245    return $this->parameters;
246  }/*}}}*/
247
248  /**
249   * Returns the normalized parameters of the request
250   *
251   * This will be all (except oauth_signature) parameters,
252   * sorted first by key, and if duplicate keys, then by
253   * value.
254   *
255   * The returned string will be all the key=value pairs
256   * concated by &.
257   *
258   * @return string
259   */
260  public function get_signable_parameters() {/*{{{*/
261    // Grab all parameters
262    $params = $this->parameters;
263         
264    // Remove oauth_signature if present
265    if (isset($params['oauth_signature'])) {
266      unset($params['oauth_signature']);
267    }
268         
269    // Urlencode both keys and values
270    $keys = array_map(array('OAuthUtil', 'urlencodeRFC3986'), array_keys($params));
271    $values = array_map(array('OAuthUtil', 'urlencodeRFC3986'), array_values($params));
272    $params = array_combine($keys, $values);
273
274    // Sort by keys (natsort)
275    uksort($params, 'strnatcmp');
276
277    // Generate key=value pairs
278    $pairs = array();
279    foreach ($params as $key=>$value ) {
280      if (is_array($value)) {
281        // If the value is an array, it's because there are multiple
282        // with the same key, sort them, then add all the pairs
283        natsort($value);
284        foreach ($value as $v2) {
285          $pairs[] = $key . '=' . $v2;
286        }
287      } else {
288        $pairs[] = $key . '=' . $value;
289      }
290    }
291         
292    // Return the pairs, concated with &
293    return implode('&', $pairs);
294  }/*}}}*/
295
296  /**
297   * Returns the base string of this request
298   *
299   * The base string defined as the method, the url
300   * and the parameters (normalized), each urlencoded
301   * and the concated with &.
302   */
303  public function get_signature_base_string() {/*{{{*/
304    $parts = array(
305      $this->get_normalized_http_method(),
306      $this->get_normalized_http_url(),
307      $this->get_signable_parameters()
308    );
309
310    $parts = array_map(array('OAuthUtil', 'urlencodeRFC3986'), $parts);
311
312    return implode('&', $parts);
313  }/*}}}*/
314
315  /**
316   * just uppercases the http method
317   */
318  public function get_normalized_http_method() {/*{{{*/
319    return strtoupper($this->http_method);
320  }/*}}}*/
321
322  /**
323   * parses the url and rebuilds it to be
324   * scheme://host/path
325   */
326  public function get_normalized_http_url() {/*{{{*/
327    $parts = parse_url($this->http_url);
328
329    $port = @$parts['port'];
330    $scheme = $parts['scheme'];
331    $host = $parts['host'];
332    $path = @$parts['path'];
333
334    $port or $port = ($scheme == 'https') ? '443' : '80';
335
336    if (($scheme == 'https' && $port != '443')
337        || ($scheme == 'http' && $port != '80')) {
338      $host = "$host:$port";
339    }
340    return "$scheme://$host$path";
341  }/*}}}*/
342
343  /**
344   * builds a url usable for a GET request
345   */
346  public function to_url() {/*{{{*/
347    $out = $this->get_normalized_http_url() . "?";
348    $out .= $this->to_postdata();
349    return $out;
350  }/*}}}*/
351
352  /**
353   * builds the data one would send in a POST request
354   */
355  public function to_postdata() {/*{{{*/
356    $total = array();
357    foreach ($this->parameters as $k => $v) {
358      $total[] = OAuthUtil::urlencodeRFC3986($k) . "=" . OAuthUtil::urlencodeRFC3986($v);
359    }
360    $out = implode("&", $total);
361    return $out;
362  }/*}}}*/
363
364  /**
365   * builds the Authorization: header
366   */
367  public function to_header($realm="") {/*{{{*/
368    $out ='"Authorization: OAuth realm="' . $realm . '",';
369    $total = array();
370    foreach ($this->parameters as $k => $v) {
371      if (substr($k, 0, 5) != "oauth") continue;
372      $out .= ',' . OAuthUtil::urlencodeRFC3986($k) . '="' . OAuthUtil::urlencodeRFC3986($v) . '"';
373    }
374    return $out;
375  }/*}}}*/
376
377  public function __toString() {/*{{{*/
378    return $this->to_url();
379  }/*}}}*/
380
381
382  public function sign_request($signature_method, $consumer, $token) {/*{{{*/
383    $this->set_parameter("oauth_signature_method", $signature_method->get_name());
384    $signature = $this->build_signature($signature_method, $consumer, $token);
385    $this->set_parameter("oauth_signature", $signature);
386  }/*}}}*/
387
388  public function build_signature($signature_method, $consumer, $token) {/*{{{*/
389    $signature = $signature_method->build_signature($this, $consumer, $token);
390    return $signature;
391  }/*}}}*/
392
393  /**
394   * util function: current timestamp
395   */
396  private static function generate_timestamp() {/*{{{*/
397    return time();
398  }/*}}}*/
399
400  /**
401   * util function: current nonce
402   */
403  private static function generate_nonce() {/*{{{*/
404    $mt = microtime();
405    $rand = mt_rand();
406
407    return md5($mt . $rand); // md5s look nicer than numbers
408  }/*}}}*/
409
410  /**
411   * util function for turning the Authorization: header into
412   * parameters, has to do some unescaping
413   */
414  private static function split_header($header) {/*{{{*/
415    // remove 'OAuth ' at the start of a header
416    $header = substr($header, 6); 
417
418    // error cases: commas in parameter values?
419    $parts = explode(",", $header);
420    $out = array();
421    foreach ($parts as $param) {
422      $param = ltrim($param);
423      // skip the "realm" param, nobody ever uses it anyway
424      if (substr($param, 0, 5) != "oauth") continue;
425
426      $param_parts = explode("=", $param);
427
428      // rawurldecode() used because urldecode() will turn a "+" in the
429      // value into a space
430      $out[$param_parts[0]] = rawurldecode(substr($param_parts[1], 1, -1));
431    }
432    return $out;
433  }/*}}}*/
434
435  /**
436   * helper to try to sort out headers for people who aren't running apache
437   */
438  private static function get_headers() {/*{{{*/
439    if (function_exists('apache_request_headers')) {
440      // we need this to get the actual Authorization: header
441      // because apache tends to tell us it doesn't exist
442      return apache_request_headers();
443    }
444    // otherwise we don't have apache and are just going to have to hope
445    // that $_SERVER actually contains what we need
446    $out = array();
447    foreach ($_SERVER as $key => $value) {
448      if (substr($key, 0, 5) == "HTTP_") {
449        // this is chaos, basically it is just there to capitalize the first
450        // letter of every word that is not an initial HTTP and strip HTTP
451        // code from przemek
452        $key = str_replace(" ", "-", ucwords(strtolower(str_replace("_", " ", substr($key, 5)))));
453        $out[$key] = $value;
454      }
455    }
456    return $out;
457  }/*}}}*/
458}/*}}}*/
459
460class OAuthServer {/*{{{*/
461  protected $timestamp_threshold = 300; // in seconds, five minutes
462  protected $version = 1.0;             // hi blaine
463  protected $signature_methods = array();
464
465  protected $data_store;
466
467  function __construct($data_store) {/*{{{*/
468    $this->data_store = $data_store;
469  }/*}}}*/
470
471  public function add_signature_method($signature_method) {/*{{{*/
472    $this->signature_methods[$signature_method->get_name()] = 
473        $signature_method;
474  }/*}}}*/
475 
476  // high level functions
477
478  /**
479   * process a request_token request
480   * returns the request token on success
481   */
482  public function fetch_request_token(&$request) {/*{{{*/
483    $this->get_version($request);
484
485    $consumer = $this->get_consumer($request);
486
487    // no token required for the initial token request
488    $token = NULL;
489
490    $this->check_signature($request, $consumer, $token);
491
492    $new_token = $this->data_store->new_request_token($consumer);
493
494    return $new_token;
495  }/*}}}*/
496
497  /**
498   * process an access_token request
499   * returns the access token on success
500   */
501  public function fetch_access_token(&$request) {/*{{{*/
502    $this->get_version($request);
503
504    $consumer = $this->get_consumer($request);
505
506    // requires authorized request token
507    $token = $this->get_token($request, $consumer, "request");
508
509    $this->check_signature($request, $consumer, $token);
510
511    $new_token = $this->data_store->new_access_token($token, $consumer);
512
513    return $new_token;
514  }/*}}}*/
515
516  /**
517   * verify an api call, checks all the parameters
518   */
519  public function verify_request(&$request) {/*{{{*/
520    $this->get_version($request);
521    $consumer = $this->get_consumer($request);
522    $token = $this->get_token($request, $consumer, "access");
523    $this->check_signature($request, $consumer, $token);
524    return array($consumer, $token);
525  }/*}}}*/
526
527  // Internals from here
528  /**
529   * version 1
530   */
531  private function get_version(&$request) {/*{{{*/
532    $version = $request->get_parameter("oauth_version");
533    if (!$version) {
534      $version = 1.0;
535    }
536    if ($version && $version != $this->version) {
537      throw new OAuthException("OAuth version '$version' not supported");
538    }
539    return $version;
540  }/*}}}*/
541
542  /**
543   * figure out the signature with some defaults
544   */
545  private function get_signature_method(&$request) {/*{{{*/
546    $signature_method = 
547        @$request->get_parameter("oauth_signature_method");
548    if (!$signature_method) {
549      $signature_method = "PLAINTEXT";
550    }
551    if (!in_array($signature_method, 
552                  array_keys($this->signature_methods))) {
553      throw new OAuthException(
554        "Signature method '$signature_method' not supported try one of the following: " . implode(", ", array_keys($this->signature_methods))
555      );     
556    }
557    return $this->signature_methods[$signature_method];
558  }/*}}}*/
559
560  /**
561   * try to find the consumer for the provided request's consumer key
562   */
563  private function get_consumer(&$request) {/*{{{*/
564    $consumer_key = @$request->get_parameter("oauth_consumer_key");
565    if (!$consumer_key) {
566      throw new OAuthException("Invalid consumer key");
567    }
568
569    $consumer = $this->data_store->lookup_consumer($consumer_key);
570    if (!$consumer) {
571      throw new OAuthException("Invalid consumer");
572    }
573
574    return $consumer;
575  }/*}}}*/
576
577  /**
578   * try to find the token for the provided request's token key
579   */
580  private function get_token(&$request, $consumer, $token_type="access") {/*{{{*/
581    $token_field = @$request->get_parameter('oauth_token');
582    $token = $this->data_store->lookup_token(
583      $consumer, $token_type, $token_field
584    );
585    if (!$token) {
586      throw new OAuthException("Invalid $token_type token: $token_field");
587    }
588    return $token;
589  }/*}}}*/
590
591  /**
592   * all-in-one function to check the signature on a request
593   * should guess the signature method appropriately
594   */
595  private function check_signature(&$request, $consumer, $token) {/*{{{*/
596    // this should probably be in a different method
597    $timestamp = @$request->get_parameter('oauth_timestamp');
598    $nonce = @$request->get_parameter('oauth_nonce');
599
600    $this->check_timestamp($timestamp);
601    $this->check_nonce($consumer, $token, $nonce, $timestamp);
602
603    $signature_method = $this->get_signature_method($request);
604
605    $signature = $request->get_parameter('oauth_signature');   
606    $valid_sig = $signature_method->check_signature(
607      $request, 
608      $consumer, 
609      $token, 
610      $signature
611    );
612
613    if (!$valid_sig) {
614      throw new OAuthException("Invalid signature");
615    }
616  }/*}}}*/
617
618  /**
619   * check that the timestamp is new enough
620   */
621  private function check_timestamp($timestamp) {/*{{{*/
622    // verify that timestamp is recentish
623    $now = time();
624    if ($now - $timestamp > $this->timestamp_threshold) {
625      throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
626    }
627  }/*}}}*/
628
629  /**
630   * check that the nonce is not repeated
631   */
632  private function check_nonce($consumer, $token, $nonce, $timestamp) {/*{{{*/
633    // verify that the nonce is uniqueish
634    $found = $this->data_store->lookup_nonce($consumer, $token, $nonce, $timestamp);
635    if ($found) {
636      throw new OAuthException("Nonce already used: $nonce");
637    }
638  }/*}}}*/
639
640
641
642}/*}}}*/
643
644class OAuthDataStore {/*{{{*/
645  function lookup_consumer($consumer_key) {/*{{{*/
646    // implement me
647  }/*}}}*/
648
649  function lookup_token($consumer, $token_type, $token) {/*{{{*/
650    // implement me
651  }/*}}}*/
652
653  function lookup_nonce($consumer, $token, $nonce, $timestamp) {/*{{{*/
654    // implement me
655  }/*}}}*/
656
657  function fetch_request_token($consumer) {/*{{{*/
658    // return a new token attached to this consumer
659  }/*}}}*/
660
661  function fetch_access_token($token, $consumer) {/*{{{*/
662    // return a new access token attached to this consumer
663    // for the user associated with this token if the request token
664    // is authorized
665    // should also invalidate the request token
666  }/*}}}*/
667
668}/*}}}*/
669
670
671/*  A very naive dbm-based oauth storage
672 */
673class SimpleOAuthDataStore extends OAuthDataStore {/*{{{*/
674  private $dbh;
675
676  function __construct($path = "oauth.gdbm") {/*{{{*/
677    $this->dbh = dba_popen($path, 'c', 'gdbm');
678  }/*}}}*/
679
680  function __destruct() {/*{{{*/
681    dba_close($this->dbh);
682  }/*}}}*/
683
684  function lookup_consumer($consumer_key) {/*{{{*/
685    $rv = dba_fetch("consumer_$consumer_key", $this->dbh);
686    if ($rv === FALSE) {
687      return NULL;
688    }
689    $obj = unserialize($rv);
690    if (!($obj instanceof OAuthConsumer)) {
691      return NULL;
692    }
693    return $obj;
694  }/*}}}*/
695
696  function lookup_token($consumer, $token_type, $token) {/*{{{*/
697    $rv = dba_fetch("${token_type}_${token}", $this->dbh);
698    if ($rv === FALSE) {
699      return NULL;
700    }
701    $obj = unserialize($rv);
702    if (!($obj instanceof OAuthToken)) {
703      return NULL;
704    }
705    return $obj;
706  }/*}}}*/
707
708  function lookup_nonce($consumer, $token, $nonce, $timestamp) {/*{{{*/
709    if (dba_exists("nonce_$nonce", $this->dbh)) {
710      return TRUE;
711    } else {
712      dba_insert("nonce_$nonce", "1", $this->dbh);
713      return FALSE;
714    }
715  }/*}}}*/
716
717  function new_token($consumer, $type="request") {/*{{{*/
718    $key = md5(time());
719    $secret = time() + time();
720    $token = new OAuthToken($key, md5(md5($secret)));
721    if (!dba_insert("${type}_$key", serialize($token), $this->dbh)) {
722      throw new OAuthException("doooom!");
723    }
724    return $token;
725  }/*}}}*/
726
727  function new_request_token($consumer) {/*{{{*/
728    return $this->new_token($consumer, "request");
729  }/*}}}*/
730
731  function new_access_token($token, $consumer) {/*{{{*/
732
733    $token = $this->new_token($consumer, 'access');
734    dba_delete("request_" . $token->key, $this->dbh);
735    return $token;
736  }/*}}}*/
737}/*}}}*/
738
739class OAuthUtil {/*{{{*/
740  public static function urlencodeRFC3986($string) {/*{{{*/
741    return str_replace('+', ' ',
742                       str_replace('%7E', '~', rawurlencode($string)));
743   
744  }/*}}}*/
745   
746
747  // This decode function isn't taking into consideration the above
748  // modifications to the encoding process. However, this method doesn't
749  // seem to be used anywhere so leaving it as is.
750  public static function urldecodeRFC3986($string) {/*{{{*/
751    return rawurldecode($string);
752  }/*}}}*/
753}/*}}}*/
754
755?>
Note: See TracBrowser for help on using the repository browser.

Sites map