1: <?php
  2: 
  3: 
  4:   5: 
  6: class OAuthExcept extends Exception {
  7:   
  8: }
  9: 
 10: class 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:   function __toString() {
 21:     return "OAuthConsumer[key=$this->key,secret=$this->secret]";
 22:   }
 23: }
 24: 
 25: class OAuthToken {
 26:   
 27:   public $key;
 28:   public $secret;
 29: 
 30:    31:  32:  33: 
 34:   function __construct($key, $secret) {
 35:     $this->key = $key;
 36:     $this->secret = $secret;
 37:   }
 38: 
 39:    40:  41:  42: 
 43:   function to_string() {
 44:     return "oauth_token=" .
 45:            OAuthUtil::urlencode_rfc3986($this->key) .
 46:            "&oauth_token_secret=" .
 47:            OAuthUtil::urlencode_rfc3986($this->secret);
 48:   }
 49: 
 50:   function __toString() {
 51:     return $this->to_string();
 52:   }
 53: }
 54: 
 55:  56:  57:  58: 
 59: abstract class OAuthSignatureMethod {
 60:    61:  62:  63: 
 64:   abstract public function get_name();
 65: 
 66:    67:  68:  69:  70:  71:  72:  73:  74:  75: 
 76:   abstract public function build_signature($request, $consumer, $token);
 77: 
 78:    79:  80:  81:  82:  83:  84:  85: 
 86:   public function check_signature($request, $consumer, $token, $signature) {
 87:     $built = $this->build_signature($request, $consumer, $token);
 88:     return $built == $signature;
 89:   }
 90: }
 91: 
 92:  93:  94:  95:  96:  97:  98: 
 99: class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod {
100:   function get_name() {
101:     return "HMAC-SHA1";
102:   }
103: 
104:   public function build_signature($request, $consumer, $token) {
105:     $base_string = $request->get_signature_base_string();
106:     $request->base_string = $base_string;
107: 
108:     $key_parts = array(
109:       $consumer->secret,
110:       ($token) ? $token->secret : ""
111:     );
112: 
113:     $key_parts = OAuthUtil::urlencode_rfc3986($key_parts);
114:     $key = implode('&', $key_parts);
115: 
116:     return base64_encode(hash_hmac('sha1', $base_string, $key, true));
117:   }
118: }
119: 
120: 121: 122: 123: 124: 
125: class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod {
126:   public function get_name() {
127:     return "PLAINTEXT";
128:   }
129: 
130:   131: 132: 133: 134: 135: 136: 137: 138: 
139:   public function build_signature($request, $consumer, $token) {
140:     $key_parts = array(
141:       $consumer->secret,
142:       ($token) ? $token->secret : ""
143:     );
144: 
145:     $key_parts = OAuthUtil::urlencode_rfc3986($key_parts);
146:     $key = implode('&', $key_parts);
147:     $request->base_string = $key;
148: 
149:     return $key;
150:   }
151: }
152: 
153: 154: 155: 156: 157: 158: 159: 160: 
161: abstract class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod {
162:   public function get_name() {
163:     return "RSA-SHA1";
164:   }
165: 
166:   
167:   
168:   
169:   
170:   
171:   
172:   protected abstract function fetch_public_cert(&$request);
173: 
174:   
175:   
176:   
177:   
178:   protected abstract function fetch_private_cert(&$request);
179: 
180:   public function build_signature($request, $consumer, $token) {
181:     $base_string = $request->get_signature_base_string();
182:     $request->base_string = $base_string;
183: 
184:     
185:     $cert = $this->fetch_private_cert($request);
186: 
187:     
188:     $privatekeyid = openssl_get_privatekey($cert);
189: 
190:     
191:     $ok = openssl_sign($base_string, $signature, $privatekeyid);
192: 
193:     
194:     openssl_free_key($privatekeyid);
195: 
196:     return base64_encode($signature);
197:   }
198: 
199:   public function check_signature($request, $consumer, $token, $signature) {
200:     $decoded_sig = base64_decode($signature);
201: 
202:     $base_string = $request->get_signature_base_string();
203: 
204:     
205:     $cert = $this->fetch_public_cert($request);
206: 
207:     
208:     $publickeyid = openssl_get_publickey($cert);
209: 
210:     
211:     $ok = openssl_verify($base_string, $decoded_sig, $publickeyid);
212: 
213:     
214:     openssl_free_key($publickeyid);
215: 
216:     return $ok == 1;
217:   }
218: }
219: 
220: class OAuthRequest {
221:   private $parameters;
222:   private $http_method;
223:   private $http_url;
224:   
225:   public $base_string;
226:   public static $version = '1.0';
227:   public static $POST_INPUT = 'php://input';
228: 
229:   function __construct($http_method, $http_url, $parameters=NULL) {
230:     @$parameters or $parameters = array();
231:     $parameters = array_merge( OAuthUtil::parse_parameters(parse_url($http_url, PHP_URL_QUERY)), $parameters);
232:     $this->parameters = $parameters;
233:     $this->http_method = $http_method;
234:     $this->http_url = $http_url;
235:   }
236: 
237: 
238:   239: 240: 
241:   public static function from_request($http_method=NULL, $http_url=NULL, $parameters=NULL) {
242:     $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on")
243:               ? 'http'
244:               : 'https';
245:     @$http_url or $http_url = $scheme .
246:                               '://' . $_SERVER['HTTP_HOST'] .
247:                               ':' .
248:                               $_SERVER['SERVER_PORT'] .
249:                               $_SERVER['REQUEST_URI'];
250:     @$http_method or $http_method = $_SERVER['REQUEST_METHOD'];
251: 
252:     
253:     
254:     
255:     
256:     if (!$parameters) {
257:       
258:       $request_headers = OAuthUtil::get_headers();
259: 
260:       
261:       $parameters = OAuthUtil::parse_parameters($_SERVER['QUERY_STRING']);
262: 
263:       
264:       
265:       if ($http_method == "POST"
266:           && @strstr($request_headers["Content-Type"],
267:                      "application/x-www-form-urlencoded")
268:           ) {
269:         $post_data = OAuthUtil::parse_parameters(
270:           file_get_contents(self::$POST_INPUT)
271:         );
272:         $parameters = array_merge($parameters, $post_data);
273:       }
274: 
275:       
276:       
277:       if (@substr($request_headers['Authorization'], 0, 6) == "OAuth ") {
278:         $header_parameters = OAuthUtil::split_header(
279:           $request_headers['Authorization']
280:         );
281:         $parameters = array_merge($parameters, $header_parameters);
282:       }
283: 
284:     }
285: 
286:     return new OAuthRequest($http_method, $http_url, $parameters);
287:   }
288: 
289:   290: 291: 
292:   public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=NULL) {
293:     @$parameters or $parameters = array();
294:     $defaults = array("oauth_version" => OAuthRequest::$version,
295:                       "oauth_nonce" => OAuthRequest::generate_nonce(),
296:                       "oauth_timestamp" => OAuthRequest::generate_timestamp(),
297:                       "oauth_consumer_key" => $consumer->key);
298:     if ($token)
299:       $defaults['oauth_token'] = $token->key;
300: 
301:     $parameters = array_merge($defaults, $parameters);
302: 
303:     return new OAuthRequest($http_method, $http_url, $parameters);
304:   }
305: 
306:   public function set_parameter($name, $value, $allow_duplicates = true) {
307:     if ($allow_duplicates && isset($this->parameters[$name])) {
308:       
309:       if (is_scalar($this->parameters[$name])) {
310:         
311:         
312:         $this->parameters[$name] = array($this->parameters[$name]);
313:       }
314: 
315:       $this->parameters[$name][] = $value;
316:     } else {
317:       $this->parameters[$name] = $value;
318:     }
319:   }
320: 
321:   public function get_parameter($name) {
322:     return isset($this->parameters[$name]) ? $this->parameters[$name] : null;
323:   }
324: 
325:   public function get_parameters() {
326:     return $this->parameters;
327:   }
328: 
329:   public function unset_parameter($name) {
330:     unset($this->parameters[$name]);
331:   }
332: 
333:   334: 335: 336: 
337:   public function get_signable_parameters() {
338:     
339:     $params = $this->parameters;
340: 
341:     
342:     
343:     if (isset($params['oauth_signature'])) {
344:       unset($params['oauth_signature']);
345:     }
346: 
347:     return OAuthUtil::build_http_query($params);
348:   }
349: 
350:   351: 352: 353: 354: 355: 356: 
357:   public function get_signature_base_string() {
358:     $parts = array(
359:       $this->get_normalized_http_method(),
360:       $this->get_normalized_http_url(),
361:       $this->get_signable_parameters()
362:     );
363: 
364:     $parts = OAuthUtil::urlencode_rfc3986($parts);
365: 
366:     return implode('&', $parts);
367:   }
368: 
369:   370: 371: 
372:   public function get_normalized_http_method() {
373:     return strtoupper($this->http_method);
374:   }
375: 
376:   377: 378: 379: 
380:   public function get_normalized_http_url() {
381:     $parts = parse_url($this->http_url);
382: 
383:     $port = @$parts['port'];
384:     $scheme = $parts['scheme'];
385:     $host = $parts['host'];
386:     $path = @$parts['path'];
387: 
388:     $port or $port = ($scheme == 'https') ? '443' : '80';
389: 
390:     if (($scheme == 'https' && $port != '443')
391:         || ($scheme == 'http' && $port != '80')) {
392:       $host = "$host:$port";
393:     }
394:     return "$scheme://$host$path";
395:   }
396: 
397:   398: 399: 
400:   public function to_url() {
401:     $post_data = $this->to_postdata();
402:     $out = $this->get_normalized_http_url();
403:     if ($post_data) {
404:       $out .= '?'.$post_data;
405:     }
406:     return $out;
407:   }
408: 
409:   410: 411: 
412:   public function to_postdata() {
413:     return OAuthUtil::build_http_query($this->parameters);
414:   }
415: 
416:   417: 418: 
419:   public function to_header($realm=null) {
420:     $first = true;
421:     if($realm) {
422:       $out = 'Authorization: OAuth realm="' . OAuthUtil::urlencode_rfc3986($realm) . '"';
423:       $first = false;
424:     } else
425:       $out = 'Authorization: OAuth';
426: 
427:     $total = array();
428:     foreach ($this->parameters as $k => $v) {
429:       if (substr($k, 0, 5) != "oauth") continue;
430:       if (is_array($v)) {
431:         throw new OAuthExcept('Arrays not supported in headers');
432:       }
433:       $out .= ($first) ? ' ' : ',';
434:       $out .= OAuthUtil::urlencode_rfc3986($k) .
435:               '="' .
436:               OAuthUtil::urlencode_rfc3986($v) .
437:               '"';
438:       $first = false;
439:     }
440:     return $out;
441:   }
442: 
443:   public function __toString() {
444:     return $this->to_url();
445:   }
446: 
447: 
448:   public function sign_request($signature_method, $consumer, $token) {
449:     $this->set_parameter(
450:       "oauth_signature_method",
451:       $signature_method->get_name(),
452:       false
453:     );
454:     $signature = $this->build_signature($signature_method, $consumer, $token);
455:     $this->set_parameter("oauth_signature", $signature, false);
456:   }
457: 
458:   public function build_signature($signature_method, $consumer, $token) {
459:     $signature = $signature_method->build_signature($this, $consumer, $token);
460:     return $signature;
461:   }
462: 
463:   464: 465: 
466:   private static function generate_timestamp() {
467:     return time();
468:   }
469: 
470:   471: 472: 
473:   private static function generate_nonce() {
474:     $mt = microtime();
475:     $rand = mt_rand();
476: 
477:     return md5($mt . $rand); 
478:   }
479: }
480: 
481: class OAuthServer {
482:   protected $timestamp_threshold = 300; 
483:   protected $version = '1.0';             
484:   protected $signature_methods = array();
485: 
486:   protected $data_store;
487: 
488:   function __construct($data_store) {
489:     $this->data_store = $data_store;
490:   }
491: 
492:   public function add_signature_method($signature_method) {
493:     $this->signature_methods[$signature_method->get_name()] =
494:       $signature_method;
495:   }
496: 
497:   
498: 
499:   500: 501: 502: 
503:   public function fetch_request_token(&$request) {
504:     $this->get_version($request);
505: 
506:     $consumer = $this->get_consumer($request);
507: 
508:     
509:     $token = NULL;
510: 
511:     $this->check_signature($request, $consumer, $token);
512: 
513:     
514:     $callback = $request->get_parameter('oauth_callback');
515:     $new_token = $this->data_store->new_request_token($consumer, $callback);
516: 
517:     return $new_token;
518:   }
519: 
520:   521: 522: 523: 
524:   public function fetch_access_token(&$request) {
525:     $this->get_version($request);
526: 
527:     $consumer = $this->get_consumer($request);
528: 
529:     
530:     $token = $this->get_token($request, $consumer, "request");
531: 
532:     $this->check_signature($request, $consumer, $token);
533: 
534:     
535:     $verifier = $request->get_parameter('oauth_verifier');
536:     $new_token = $this->data_store->new_access_token($token, $consumer, $verifier);
537: 
538:     return $new_token;
539:   }
540: 
541:   542: 543: 
544:   public function verify_request(&$request) {
545:     $this->get_version($request);
546:     $consumer = $this->get_consumer($request);
547:     $token = $this->get_token($request, $consumer, "access");
548:     $this->check_signature($request, $consumer, $token);
549:     return array($consumer, $token);
550:   }
551: 
552:   
553:   554: 555: 
556:   private function get_version(&$request) {
557:     $version = $request->get_parameter("oauth_version");
558:     if (!$version) {
559:       
560:       
561:       $version = '1.0';
562:     }
563:     if ($version !== $this->version) {
564:       throw new OAuthExcept("OAuth version '$version' not supported");
565:     }
566:     return $version;
567:   }
568: 
569:   570: 571: 
572:   private function get_signature_method(&$request) {
573:     $signature_method =
574:         @$request->get_parameter("oauth_signature_method");
575: 
576:     if (!$signature_method) {
577:       
578:       
579:       throw new OAuthExcept('No signature method parameter. This parameter is required');
580:     }
581: 
582:     if (!in_array($signature_method,
583:                   array_keys($this->signature_methods))) {
584:       throw new OAuthExcept(
585:         "Signature method '$signature_method' not supported " .
586:         "try one of the following: " .
587:         implode(", ", array_keys($this->signature_methods))
588:       );
589:     }
590:     return $this->signature_methods[$signature_method];
591:   }
592: 
593:   594: 595: 
596:   private function get_consumer(&$request) {
597:     $consumer_key = @$request->get_parameter("oauth_consumer_key");
598:     if (!$consumer_key) {
599:       throw new OAuthExcept("Invalid consumer key");
600:     }
601: 
602:     $consumer = $this->data_store->lookup_consumer($consumer_key);
603:     if (!$consumer) {
604:       throw new OAuthExcept("Invalid consumer");
605:     }
606: 
607:     return $consumer;
608:   }
609: 
610:   611: 612: 
613:   private function get_token(&$request, $consumer, $token_type="access") {
614:     $token_field = @$request->get_parameter('oauth_token');
615:     $token = $this->data_store->lookup_token(
616:       $consumer, $token_type, $token_field
617:     );
618:     if (!$token) {
619:       throw new OAuthExcept("Invalid $token_type token: $token_field");
620:     }
621:     return $token;
622:   }
623: 
624:   625: 626: 627: 
628:   private function check_signature(&$request, $consumer, $token) {
629:     
630:     $timestamp = @$request->get_parameter('oauth_timestamp');
631:     $nonce = @$request->get_parameter('oauth_nonce');
632: 
633:     $this->check_timestamp($timestamp);
634:     $this->check_nonce($consumer, $token, $nonce, $timestamp);
635: 
636:     $signature_method = $this->get_signature_method($request);
637: 
638:     $signature = $request->get_parameter('oauth_signature');
639:     $valid_sig = $signature_method->check_signature(
640:       $request,
641:       $consumer,
642:       $token,
643:       $signature
644:     );
645: 
646:     if (!$valid_sig) {
647:       throw new OAuthExcept("Invalid signature");
648:     }
649:   }
650: 
651:   652: 653: 
654:   private function check_timestamp($timestamp) {
655:     if( ! $timestamp )
656:       throw new OAuthExcept(
657:         'Missing timestamp parameter. The parameter is required'
658:       );
659:     
660:     
661:     $now = time();
662:     if (abs($now - $timestamp) > $this->timestamp_threshold) {
663:       throw new OAuthExcept(
664:         "Expired timestamp, yours $timestamp, ours $now"
665:       );
666:     }
667:   }
668: 
669:   670: 671: 
672:   private function check_nonce($consumer, $token, $nonce, $timestamp) {
673:     if( ! $nonce )
674:       throw new OAuthExcept(
675:         'Missing nonce parameter. The parameter is required'
676:       );
677: 
678:     
679:     $found = $this->data_store->lookup_nonce(
680:       $consumer,
681:       $token,
682:       $nonce,
683:       $timestamp
684:     );
685:     if ($found) {
686:       throw new OAuthExcept("Nonce already used: $nonce");
687:     }
688:   }
689: 
690: }
691: 
692: class OAuthDataStore {
693:   function lookup_consumer($consumer_key) {
694:     
695:   }
696: 
697:   function lookup_token($consumer, $token_type, $token) {
698:     
699:   }
700: 
701:   function lookup_nonce($consumer, $token, $nonce, $timestamp) {
702:     
703:   }
704: 
705:   function new_request_token($consumer, $callback = null) {
706:     
707:   }
708: 
709:   function new_access_token($token, $consumer, $verifier = null) {
710:     
711:     
712:     
713:     
714:   }
715: 
716: }
717: 
718: class OAuthUtil {
719:   public static function urlencode_rfc3986($input) {
720:   if (is_array($input)) {
721:     return array_map(array('OAuthUtil', 'urlencode_rfc3986'), $input);
722:   } else if (is_scalar($input)) {
723:     return str_replace(
724:       '+',
725:       ' ',
726:       str_replace('%7E', '~', rawurlencode($input))
727:     );
728:   } else {
729:     return '';
730:   }
731: }
732: 
733: 
734:   
735:   
736:   
737:   public static function urldecode_rfc3986($string) {
738:     return urldecode($string);
739:   }
740: 
741:   
742:   
743:   
744:   public static function split_header($header, $only_allow_oauth_parameters = true) {
745:     $pattern = '/(([-_a-z]*)=("([^"]*)"|([^,]*)),?)/';
746:     $offset = 0;
747:     $params = array();
748:     while (preg_match($pattern, $header, $matches, PREG_OFFSET_CAPTURE, $offset) > 0) {
749:       $match = $matches[0];
750:       $header_name = $matches[2][0];
751:       $header_content = (isset($matches[5])) ? $matches[5][0] : $matches[4][0];
752:       if (preg_match('/^oauth_/', $header_name) || !$only_allow_oauth_parameters) {
753:         $params[$header_name] = OAuthUtil::urldecode_rfc3986($header_content);
754:       }
755:       $offset = $match[1] + strlen($match[0]);
756:     }
757: 
758:     if (isset($params['realm'])) {
759:       unset($params['realm']);
760:     }
761: 
762:     return $params;
763:   }
764: 
765:   
766:   public static function get_headers() {
767:     if (function_exists('apache_request_headers')) {
768:       
769:       
770:       $headers = apache_request_headers();
771: 
772:       
773:       
774:       
775:       
776:       $out = array();
777:       foreach( $headers AS $key => $value ) {
778:         $key = str_replace(
779:             " ",
780:             "-",
781:             ucwords(strtolower(str_replace("-", " ", $key)))
782:           );
783:         $out[$key] = $value;
784:       }
785:     } else {
786:       
787:       
788:       $out = array();
789:       if( isset($_SERVER['CONTENT_TYPE']) )
790:         $out['Content-Type'] = $_SERVER['CONTENT_TYPE'];
791:       if( isset($_ENV['CONTENT_TYPE']) )
792:         $out['Content-Type'] = $_ENV['CONTENT_TYPE'];
793: 
794:       foreach ($_SERVER as $key => $value) {
795:         if (substr($key, 0, 5) == "HTTP_") {
796:           
797:           
798:           
799:           $key = str_replace(
800:             " ",
801:             "-",
802:             ucwords(strtolower(str_replace("_", " ", substr($key, 5))))
803:           );
804:           $out[$key] = $value;
805:         }
806:       }
807:     }
808:     return $out;
809:   }
810: 
811:   
812:   
813:   
814:   public static function parse_parameters( $input ) {
815:     if (!isset($input) || !$input) return array();
816: 
817:     $pairs = explode('&', $input);
818: 
819:     $parsed_parameters = array();
820:     foreach ($pairs as $pair) {
821:       $split = explode('=', $pair, 2);
822:       $parameter = OAuthUtil::urldecode_rfc3986($split[0]);
823:       $value = isset($split[1]) ? OAuthUtil::urldecode_rfc3986($split[1]) : '';
824: 
825:       if (isset($parsed_parameters[$parameter])) {
826:         
827:         
828: 
829:         if (is_scalar($parsed_parameters[$parameter])) {
830:           
831:           
832:           $parsed_parameters[$parameter] = array($parsed_parameters[$parameter]);
833:         }
834: 
835:         $parsed_parameters[$parameter][] = $value;
836:       } else {
837:         $parsed_parameters[$parameter] = $value;
838:       }
839:     }
840:     return $parsed_parameters;
841:   }
842: 
843:   public static function build_http_query($params) {
844:     if (!$params) return '';
845: 
846:     
847:     $keys = OAuthUtil::urlencode_rfc3986(array_keys($params));
848:     $values = OAuthUtil::urlencode_rfc3986(array_values($params));
849:     $params = array_combine($keys, $values);
850: 
851:     
852:     
853:     uksort($params, 'strcmp');
854: 
855:     $pairs = array();
856:     foreach ($params as $parameter => $value) {
857:       if (is_array($value)) {
858:         
859:         
860:         natsort($value);
861:         foreach ($value as $duplicate_value) {
862:           $pairs[] = $parameter . '=' . $duplicate_value;
863:         }
864:       } else {
865:         $pairs[] = $parameter . '=' . $value;
866:       }
867:     }
868:     
869:     
870:     return implode('&', $pairs);
871:   }
872: }
873: