* @date Nov 16, 2007 12:20:31 PM * * The MIT License * * Copyright (c) 2007-2008 Mediamatic Lab * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ require_once dirname(__FILE__) . '/OAuthException.php'; /** * Object to parse an incoming OAuth request or prepare an outgoing OAuth request */ class OAuthRequest { /* the realm for this request */ protected $realm; /* all the parameters, RFC3986 encoded name/value pairs */ protected $param = array(); /* the parsed request uri */ protected $uri_parts; /* the raw request uri */ protected $uri; /* the request headers */ protected $headers; /* the request method */ protected $method; /* the body of the OAuth request */ protected $body; /** * Construct from the current request. Useful for checking the signature of a request. * When not supplied with any parameters this will use the current request. * * @param string uri might include parameters * @param string method GET, PUT, POST etc. * @param string parameters additional post parameters, urlencoded (RFC1738) * @param array headers headers for request * @param string body optional body of the OAuth request (POST or PUT) */ function __construct ( $uri = null, $method = 'GET', $parameters = '', $headers = array(), $body = null ) { if (empty($uri)) { if (is_object($_SERVER)) { // Tainted arrays - the normal stuff in anyMeta $method = $_SERVER->REQUEST_METHOD->getRawUnsafe(); $uri = $_SERVER->REQUEST_URI->getRawUnsafe(); } else { // non anyMeta systems $method = $_SERVER['REQUEST_METHOD']; $uri = $_SERVER['REQUEST_URI']; } $headers = getallheaders(); $parameters = ''; $this->method = strtoupper($method); // If this is a post then also check the posted variables if (strcasecmp($method, 'POST') == 0) { /* // TODO: what to do with 'multipart/form-data'? if ($this->getRequestContentType() == 'multipart/form-data') { throw new OAuthException('Unsupported POST content type, expected "application/x-www-form-urlencoded" got "'.@$_SERVER['CONTENT_TYPE'].'"'); } */ if ($this->getRequestContentType() == 'application/x-www-form-urlencoded') { // Get the posted body (when available) if (!isset($headers['X-OAuth-Test'])) { $parameters .= $this->getRequestBody(); } } else { $body = $this->getRequestBody(); } } else if (strcasecmp($method, 'PUT') == 0) { $body = $this->getRequestBody(); } } $this->method = strtoupper($method); $this->headers = $headers; // Store the values, prepare for oauth $this->uri = $uri; $this->body = $body; $this->parseUri($parameters); $this->parseHeaders(); $this->transcodeParams(); } /** * Return the signature base string. * Note that we can't use rawurlencode due to specified use of RFC3986. * * @return string */ function signatureBaseString () { $sig = array(); $sig[] = $this->method; $sig[] = $this->getRequestUrl(); $sig[] = $this->getNormalizedParams(); return implode('&', array_map(array($this, 'urlencode'), $sig)); } /** * Calculate the signature of the request, using the method in oauth_signature_method. * The signature is returned encoded in the form as used in the url. So the base64 and * urlencoding has been done. * * @param string consumer_secret * @param string token_secret * @exception when not all parts available * @return string */ function calculateSignature ( $consumer_secret, $token_secret, $token_type = 'access' ) { $required = array( 'oauth_consumer_key', 'oauth_signature_method', 'oauth_timestamp', 'oauth_nonce' ); if ($token_type !== false) { $required[] = 'oauth_token'; } foreach ($required as $req) { if (!isset($this->param[$req])) { throw new OAuthException('Can\'t sign request, missing parameter "'.$req.'"'); } } $this->checks(); $base = $this->signatureBaseString(); $signature = $this->calculateDataSignature($base, $consumer_secret, $token_secret, $this->param['oauth_signature_method']); return $signature; } /** * Calculate the signature of a string. * Uses the signature method from the current parameters. * * @param string data * @param string consumer_secret * @param string token_secret * @param string signature_method * @exception OAuthException thrown when the signature method is unknown * @return string signature */ function calculateDataSignature ( $data, $consumer_secret, $token_secret, $signature_method ) { if (is_null($data)) { $data = ''; } $sig = $this->getSignatureMethod($signature_method); return $sig->signature($this, $data, $consumer_secret, $token_secret); } /** * Select a signature method from the list of available methods. * We try to check the most secure methods first. * * @todo Let the signature method tell us how secure it is * @param array methods * @exception OAuthException when we don't support any method in the list * @return string */ public function selectSignatureMethod ( $methods ) { if (in_array('HMAC-SHA1', $methods)) { $method = 'HMAC-SHA1'; } else if (in_array('MD5', $methods)) { $method = 'MD5'; } else { $method = false; foreach ($methods as $m) { $m = strtoupper($m); $m = preg_replace('/[^A-Z0-9]/', '_', $m); if (file_exists(dirname(__FILE__).'/signature_method/OAuthSignatureMethod_'.$m.'.php')) { $method = $m; break; } } if (empty($method)) { throw new OAuthException('None of the signing methods is supported.'); } } return $method; } /** * Fetch the signature object used for calculating and checking the signature base string * * @param string method * @return OAuthSignatureMethod object */ function getSignatureMethod ( $method ) { $m = strtoupper($method); $m = preg_replace('/[^A-Z0-9]/', '_', $m); $class = 'OAuthSignatureMethod_'.$m; if (file_exists(dirname(__FILE__).'/signature_method/'.$class.'.php')) { require_once dirname(__FILE__).'/signature_method/'.$class.'.php'; $sig = new $class(); } else { throw new OAuthException('Unsupported signature method "'.$m.'".'); } return $sig; } /** * Perform some sanity checks. * * @exception OAuthException thrown when sanity checks failed */ function checks () { if (isset($this->param['oauth_version'])) { $version = $this->urldecode($this->param['oauth_version']); if ($version != '1.0') { throw new OAuthException('Expected OAuth version 1.0, got "'.$this->param['oauth_version'].'"'); } } } /** * Return the request method * * @return string */ function getMethod () { return $this->method; } /** * Return the complete parameter string for the signature check. * All parameters are correctly urlencoded and sorted on name and value * * @return string */ function getNormalizedParams () { /* // sort by name, then by value // (needed when we start allowing multiple values with the same name) $keys = array_keys($this->param); $values = array_values($this->param); array_multisort($keys, SORT_ASC, $values, SORT_ASC); */ $params = $this->param; $normalized = array(); ksort($params); foreach ($params as $key => $value) { // all names and values are already urlencoded, exclude the oauth signature if ($key != 'oauth_signature') { if (is_array($value)) { $value_sort = $value; sort($value_sort); foreach ($value_sort as $v) { $normalized[] = $key.'='.$v; } } else { $normalized[] = $key.'='.$value; } } } return implode('&', $normalized); } /** * Return the normalised url for signature checks */ function getRequestUrl () { $url = $this->uri_parts['scheme'] . '://' . $this->uri_parts['user'] . (!empty($this->uri_parts['pass']) ? ':' : '') . $this->uri_parts['pass'] . (!empty($this->uri_parts['user']) ? '@' : '') . $this->uri_parts['host']; if ( $this->uri_parts['port'] && $this->uri_parts['port'] != $this->defaultPortForScheme($this->uri_parts['scheme'])) { $url .= ':'.$this->uri_parts['port']; } if (!empty($this->uri_parts['path'])) { $url .= $this->uri_parts['path']; } return $url; } /** * Get a parameter, value is always urlencoded * * @param string name * @param boolean urldecode set to true to decode the value upon return * @return string value false when not found */ function getParam ( $name, $urldecode = false ) { if (isset($this->param[$name])) { $s = $this->param[$name]; } else if (isset($this->param[$this->urlencode($name)])) { $s = $this->param[$this->urlencode($name)]; } else { $s = false; } if (!empty($s) && $urldecode) { if (is_array($s)) { $s = array_map(array($this,'urldecode'), $s); } else { $s = $this->urldecode($s); } } return $s; } /** * Set a parameter * * @param string name * @param string value * @param boolean encoded set to true when the values are already encoded */ function setParam ( $name, $value, $encoded = false ) { if (!$encoded) { $name_encoded = $this->urlencode($name); if (is_array($value)) { foreach ($value as $v) { $this->param[$name_encoded][] = $this->urlencode($v); } } else { $this->param[$name_encoded] = $this->urlencode($value); } } else { $this->param[$name] = $value; } } /** * Re-encode all parameters so that they are encoded using RFC3986. * Updates the $this->param attribute. */ protected function transcodeParams () { $params = $this->param; $this->param = array(); foreach ($params as $name=>$value) { if (is_array($value)) { $this->param[$this->urltranscode($name)] = array_map(array($this,'urltranscode'), $value); } else { $this->param[$this->urltranscode($name)] = $this->urltranscode($value); } } } /** * Return the body of the OAuth request. * * @return string null when no body */ function getBody () { return $this->body; } /** * Return the body of the OAuth request. * * @return string null when no body */ function setBody ( $body ) { $this->body = $body; } /** * Parse the uri into its parts. Fill in the missing parts. * * @todo check for the use of https, right now we default to http * @todo support for multiple occurences of parameters * @param string $parameters optional extra parameters (from eg the http post) */ protected function parseUri ( $parameters ) { $ps = parse_url($this->uri); // Get the current/requested method if (empty($ps['scheme'])) { $ps['scheme'] = 'http'; } else { $ps['scheme'] = strtolower($ps['scheme']); } // Get the current/requested host if (empty($ps['host'])) { if (isset($_SERVER['HTTP_HOST'])) { $ps['host'] = $_SERVER['HTTP_HOST']; } else { $ps['host'] = ''; } } $ps['host'] = mb_strtolower($ps['host']); if (!preg_match('/^[a-z0-9\.\-]+$/', $ps['host'])) { throw new OAuthException('Unsupported characters in host name'); } // Get the port we are talking on if (empty($ps['port'])) { $ps['port'] = $this->defaultPortForScheme($ps['scheme']); } if (empty($ps['user'])) { $ps['user'] = ''; } if (empty($ps['pass'])) { $ps['pass'] = ''; } if (empty($ps['path'])) { $ps['path'] = '/'; } if (empty($ps['query'])) { $ps['query'] = ''; } if (empty($ps['fragment'])) { $ps['fragment'] = ''; } // Now all is complete - parse all parameters foreach (array($ps['query'], $parameters) as $params) { if (strlen($params) > 0) { $params = explode('&', $params); foreach ($params as $p) { @list($name, $value) = explode('=', $p, 2); $this->param[$name] = $value; } } } $this->uri_parts = $ps; } /** * Return the default port for a scheme * * @param string scheme * @return int */ protected function defaultPortForScheme ( $scheme ) { switch ($scheme) { case 'http': return 80; case 'https': return 43; default: throw new OAuthException('Unsupported scheme type, expected http or https, got "'.$scheme.'"'); break; } } /** * Encode a string according to the RFC3986 * * @param string s * @return string */ function urlencode ( $s ) { if ($s === false) { return $s; } else { return str_replace('%7E', '~', rawurlencode($s)); } } /** * Decode a string according to RFC3986. * Also correctly decodes RFC1738 urls. * * @param string s * @return string */ function urldecode ( $s ) { if ($s === false) { return $s; } else { return rawurldecode($s); } } /** * urltranscode - make sure that a value is encoded using RFC3986. * We use a basic urldecode() function so that any use of '+' as the * encoding of the space character is correctly handled. * * @param string s * @return string */ function urltranscode ( $s ) { if ($s === false) { return $s; } else { return $this->urlencode(urldecode($s)); } } /** * Parse the oauth parameters from the request headers * Looks for something like: * * Authorization: OAuth realm="http://photos.example.net/authorize", * oauth_consumer_key="dpf43f3p2l4k3l03", * oauth_token="nnch734d00sl2jdk", * oauth_signature_method="HMAC-SHA1", * oauth_signature="tR3%2BTy81lMeYAr%2FFid0kMTYa%2FWM%3D", * oauth_timestamp="1191242096", * oauth_nonce="kllo9940pd9333jh", * oauth_version="1.0" */ private function parseHeaders () { /* $this->headers['Authorization'] = 'OAuth realm="http://photos.example.net/authorize", oauth_consumer_key="dpf43f3p2l4k3l03", oauth_token="nnch734d00sl2jdk", oauth_signature_method="HMAC-SHA1", oauth_signature="tR3%2BTy81lMeYAr%2FFid0kMTYa%2FWM%3D", oauth_timestamp="1191242096", oauth_nonce="kllo9940pd9333jh", oauth_version="1.0"'; */ if (isset($this->headers['Authorization'])) { $auth = trim($this->headers['Authorization']); if (strncasecmp($auth, 'OAuth', 4) == 0) { $vs = explode(',', substr($auth, 6)); foreach ($vs as $v) { if (strpos($v, '=')) { $v = trim($v); list($name,$value) = explode('=', $v, 2); if (!empty($value) && $value{0} == '"' && substr($value, -1) == '"') { $value = substr(substr($value, 1), 0, -1); } if (strcasecmp($name, 'realm') == 0) { $this->realm = $value; } else { $this->param[$name] = $value; } } } } } } /** * Fetch the content type of the current request * * @return string */ private function getRequestContentType () { $content_type = 'application/octet-stream'; if (!empty($_SERVER) && array_key_exists('CONTENT_TYPE', $_SERVER)) { list($content_type) = explode(';', $_SERVER['CONTENT_TYPE']); } return trim($content_type); } /** * Get the body of a POST or PUT. * * Used for fetching the post parameters and to calculate the body signature. * * @return string null when no body present (or wrong content type for body) */ private function getRequestBody () { $body = null; if ($this->method == 'POST' || $this->method == 'PUT') { $body = ''; $fh = @fopen('php://input', 'r'); if ($fh) { while (!feof($fh)) { $s = fread($fh, 1024); if (is_string($s)) { $body .= $s; } } fclose($fh); } } return $body; } /** * Simple function to perform a redirect (GET). * Redirects the User-Agent, does not return. * * @param string uri * @param array params parameters, urlencoded * @exception OAuthException when redirect uri is illegal */ public function redirect ( $uri, $params ) { if (!empty($params)) { $q = array(); foreach ($params as $name=>$value) { $q[] = $name.'='.$value; } $q_s = implode('&', $q); if (strpos($uri, '?')) { $uri .= '&'.$q_s; } else { $uri .= '?'.$q_s; } } // simple security - multiline location headers can inject all kinds of extras $uri = preg_replace('/\s/', '%20', $uri); if (strncasecmp($uri, 'http://', 7) && strncasecmp($uri, 'https://', 8)) { if (strpos($uri, '://')) { throw new OAuthException('Illegal protocol in redirect uri '.$uri); } $uri = 'http://'.$uri; } header('HTTP/1.1 302 Found'); header('Location: '.$uri); echo ''; exit(); } } /* vi:set ts=4 sts=4 sw=4 binary noeol: */ ?>