3 * Licensed to the Apache Software Foundation (ASF) under one
4 * or more contributor license agreements. See the NOTICE file
5 * distributed with this work for additional information
6 * regarding copyright ownership. The ASF licenses this file
7 * to you under the Apache License, Version 2.0 (the
8 * "License"); you may not use this file except in compliance
9 * with the License. You may obtain a copy of the License at
11 * http://www.apache.org/licenses/LICENSE-2.0
13 * Unless required by applicable law or agreed to in writing,
14 * software distributed under the License is distributed on an
15 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 * KIND, either express or implied. See the License for the
17 * specific language governing permissions and limitations
22 * Implements signed fetch based on the OAuth request signing algorithm.
24 * Subclasses can override signMessage to use their own crypto if they don't
25 * like the oauth.net code for some reason.
27 * Instances of this class are only accessed by a single thread at a time,
28 * but instances may be created by multiple threads.
30 class SigningFetcher
extends RemoteContentFetcher
{
32 protected static $OPENSOCIAL_OWNERID = "opensocial_owner_id";
33 protected static $OPENSOCIAL_VIEWERID = "opensocial_viewer_id";
34 protected static $OPENSOCIAL_APPID = "opensocial_app_id";
35 protected static $XOAUTH_PUBLIC_KEY = "xoauth_signature_publickey";
36 protected static $ALLOWED_PARAM_NAME = '^[-_[:alnum:]]+$';
39 * Private key we pass to the OAuth RSA_SHA1 algorithm.This can be a
40 * PrivateKey object, or a PEM formatted private key, or a DER encoded byte
41 * array for the private key.(No, really, they accept any of them.)
43 protected $privateKeyObject;
46 * The name of the key, included in the fetch to help with key rotation.
51 * @var RemoteContentFetcher
56 * Constructor based on signing with the given PrivateKey object.
58 * @param RemoteContentFetcher $fetcher
59 * @param keyName name of the key to include in the request
60 * @param privateKey the key to use for the signing
61 * @return SigningFetcher
63 public static function makeFromPrivateKey(RemoteContentFetcher
$fetcher, $keyName, $privateKey) {
64 return new SigningFetcher($fetcher, $keyName, $privateKey);
68 * Constructor based on signing with the given PrivateKey object.
70 * @param RemoteContentFetcher $fetcher
71 * @param keyName name of the key to include in the request
72 * @param privateKey base64 encoded private key
73 * @return SigningFetcher
75 public static function makeFromB64PrivateKey(RemoteContentFetcher
$fetcher, $keyName, $privateKey) {
76 return new SigningFetcher($fetcher, $keyName, $privateKey);
80 * Constructor based on signing with the given PrivateKey object.
82 * @param RemoteContentFetcher $fetcher
83 * @param keyName name of the key to include in the request
84 * @param privateKey DER encoded private key
85 * @return SigningFetcher
87 public static function makeFromPrivateKeyBytes(RemoteContentFetcher
$fetcher, $keyName, $privateKey) {
88 return new SigningFetcher($fetcher, $keyName, $privateKey);
91 protected function __construct(RemoteContentFetcher
$fetcher, $keyName, $privateKeyObject) {
92 $this->fetcher
= $fetcher;
93 $this->keyName
= $keyName;
94 $this->privateKeyObject
= $privateKeyObject;
97 public function fetchRequest(RemoteContentRequest
$request) {
98 $this->signRequest($request);
99 return $this->fetcher
->fetchRequest($request);
102 public function multiFetchRequest(Array $requests) {
103 foreach ($requests as $request) {
104 $this->signRequest($request);
106 return $this->fetcher
->multiFetchRequest($requests);
109 private function signRequest(RemoteContentRequest
$request) {
110 $url = $request->getUrl();
111 $method = $request->getMethod();
113 // Parse the request into parameters for OAuth signing, stripping out
114 // any OAuth or OpenSocial parameters injected by the client
115 $parsedUri = parse_url($url);
117 $queryParams = array();
118 if (isset($parsedUri['query'])) {
119 parse_str($parsedUri['query'], $queryParams);
120 // strip out all opensocial_* and oauth_* params so they can't be spoofed by the client
121 foreach ($queryParams as $key => $val) {
122 if ((strtolower(substr($key, 0, strlen('opensocial_'))) == 'opensocial_') ||
(strtolower(substr($key, 0, strlen('oauth_'))) == 'oauth_')) {
123 unset($queryParams[$key]);
126 $queryParams = $this->sanitize($queryParams);
128 $contentType = $request->getHeader('Content-Type');
129 $signBody = (stripos($contentType, 'application/x-www-form-urlencoded') !== false ||
$contentType == null);
130 if ($request->getPostBody()) {
132 $postParams = array();
133 // on normal application/x-www-form-urlencoded type post's encode and parse the post vars
134 parse_str($request->getPostBody(), $postParams);
135 $postParams = $this->sanitize($postParams);
137 // on any other content-type of post (application/{json,xml,xml+atom}) use the body signing hash
138 // see http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/drafts/4/spec.html for details
139 $queryParams['oauth_body_hash'] = sha1($request->getPostBody());
142 $msgParams = array();
143 $msgParams = array_merge($msgParams, $queryParams);
145 $msgParams = array_merge($msgParams, $postParams);
147 $this->addOpenSocialParams($msgParams, $request->getToken());
148 $this->addOAuthParams($msgParams, $request->getToken());
149 $consumer = new OAuthConsumer(NULL, NULL, NULL);
150 $consumer->setProperty(OAuthSignatureMethod_RSA_SHA1
::$PRIVATE_KEY, $this->privateKeyObject
);
151 $signatureMethod = new OAuthSignatureMethod_RSA_SHA1();
152 $req_req = OAuthRequest
::from_consumer_and_token($consumer, NULL, $method, $resource, $msgParams);
153 $req_req->sign_request($signatureMethod, $consumer, NULL);
154 // Rebuild the query string, including all of the parameters we added.
155 // We have to be careful not to copy POST parameters into the query.
156 // If post and query parameters share a name, they end up being removed
160 if ($method == 'POST' && $signBody) {
161 foreach ($postParams as $key => $param) {
162 $forPost[$key] = $param;
163 if ($postData === false) {
166 $postData[] = OAuthUtil
::urlencodeRFC3986($key) . "=" . OAuthUtil
::urlencodeRFC3986($param);
168 if ($postData !== false) {
169 $postData = implode("&", $postData);
173 foreach ($req_req->get_parameters() as $key => $param) {
174 if (! isset($forPost[$key])) {
175 $newQuery .= urlencode($key) . '=' . urlencode($param) . '&';
178 // and stick on the original query params too
179 if (isset($parsedUri['query']) && ! empty($parsedUri['query'])) {
181 parse_str($parsedUri['query'], $oldQuery);
182 foreach ($oldQuery as $key => $val) {
183 $newQuery .= urlencode($key) . '=' . urlencode($val) . '&';
186 // Careful here; the OAuth form encoding scheme is slightly different than
187 // the normal form encoding scheme, so we have to use the OAuth library
188 // formEncode method.
189 $url = $parsedUri['scheme'] . '://' . $parsedUri['host'] . (isset($parsedUri['port']) ?
':' . $parsedUri['port'] : '') . $parsedUri['path'] . '?' . $newQuery;
190 $request->setUri($url);
192 $request->setPostBody($postData);
194 } catch (Exception
$e) {
195 throw new GadgetException($e);
199 private function addOpenSocialParams(&$msgParams, SecurityToken
$token) {
200 $owner = $token->getOwnerId();
201 if ($owner != null) {
202 $msgParams[SigningFetcher
::$OPENSOCIAL_OWNERID] = $owner;
204 $viewer = $token->getViewerId();
205 if ($viewer != null) {
206 $msgParams[SigningFetcher
::$OPENSOCIAL_VIEWERID] = $viewer;
208 $app = $token->getAppId();
210 $msgParams[SigningFetcher
::$OPENSOCIAL_APPID] = $app;
214 private function addOAuthParams(&$msgParams, SecurityToken
$token) {
215 $msgParams[OAuth
::$OAUTH_TOKEN] = '';
216 $domain = $token->getDomain();
217 if ($domain != null) {
218 $msgParams[OAuth
::$OAUTH_CONSUMER_KEY] = $domain;
220 if ($this->keyName
!= null) {
221 $msgParams[SigningFetcher
::$XOAUTH_PUBLIC_KEY] = $this->keyName
;
223 $nonce = OAuthRequest
::generate_nonce();
224 $msgParams[OAuth
::$OAUTH_NONCE] = $nonce;
226 $msgParams[OAuth
::$OAUTH_TIMESTAMP] = $timestamp;
227 $msgParams[OAuth
::$OAUTH_SIGNATURE_METHOD] = OAuth
::$RSA_SHA1;
231 * Strip out any owner or viewer id passed by the client.
233 private function sanitize($params) {
235 foreach ($params as $key => $p) {
236 if ($this->allowParam($key)) {
243 private function allowParam($paramName) {
244 $canonParamName = strtolower($paramName);
245 // Exclude the fields which are only used to tell the proxy what to do
246 // and the fields which should be added by signing the request later on
247 if ($canonParamName == "output" ||
$canonParamName == "httpmethod" ||
$canonParamName == "authz" ||
$canonParamName == "st" ||
$canonParamName == "headers" ||
$canonParamName == "url" ||
$canonParamName == "contenttype" ||
$canonParamName == "postdata" ||
$canonParamName == "numentries" ||
$canonParamName == "getsummaries" ||
$canonParamName == "signowner" ||
$canonParamName == "signviewer" ||
$canonParamName == "gadget" ||
$canonParamName == "bypassspeccache" ||
substr($canonParamName, 0, 5) == "oauth" ||
substr($canonParamName, 0, 6) == "xoauth" ||
substr($canonParamName, 0, 9) == "opensocial") {
250 // make a last sanity check on the key of the data by using a regular expression
251 return ereg(SigningFetcher
::$ALLOWED_PARAM_NAME, $canonParamName);