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 * Authentication token for the user and gadget making the request.
44 * Private key we pass to the OAuth RSA_SHA1 algorithm.This can be a
45 * PrivateKey object, or a PEM formatted private key, or a DER encoded byte
46 * array for the private key.(No, really, they accept any of them.)
48 protected $privateKeyObject;
51 * The name of the key, included in the fetch to help with key rotation.
56 * Constructor based on signing with the given PrivateKey object.
58 * @param authToken verified gadget security token
59 * @param keyName name of the key to include in the request
60 * @param privateKey the key to use for the signing
62 public static function makeFromPrivateKey($next, $authToken, $keyName, $privateKey) {
63 return new SigningFetcher($next, $authToken, $keyName, $privateKey);
67 * Constructor based on signing with the given PrivateKey object.
69 * @param authToken verified gadget security token
70 * @param keyName name of the key to include in the request
71 * @param privateKey base64 encoded private key
73 public static function makeFromB64PrivateKey($next, $authToken, $keyName, $privateKey) {
74 return new SigningFetcher($next, $authToken, $keyName, $privateKey);
78 * Constructor based on signing with the given PrivateKey object.
80 * @param authToken verified gadget security token
81 * @param keyName name of the key to include in the request
82 * @param privateKey DER encoded private key
84 public static function makeFromPrivateKeyBytes($next, $authToken, $keyName, $privateKey) {
85 return new SigningFetcher($next, $authToken, $keyName, $privateKey);
88 protected function __construct($next, $authToken, $keyName, $privateKeyObject) {
89 parent
::setNextFetcher($next);
90 $this->authToken
= $authToken;
91 $this->keyName
= $keyName;
92 $this->privateKeyObject
= $privateKeyObject;
95 public function fetchRequest(RemoteContentRequest
$request) {
96 return $this->getNextFetcher()->fetchRequest($request);
99 public function fetch($url, $method) {
100 $signed = $this->signRequest($url, $method);
101 return $this->getNextFetcher()->fetchRequest($signed);
104 public function multiFetchRequest(Array $requests) {
105 return $this->getNextFetcher()->multiFetchRequest($requests);
108 public function signRequest($url, $method) {
110 // Parse the request into parameters for OAuth signing, stripping out
111 // any OAuth or OpenSocial parameters injected by the client
112 $parsedUri = parse_url($url);
114 $queryParams = $this->sanitize($_GET);
115 $postParams = $this->sanitize($_POST);
116 // The data that is supposed to be posted to the target page is contained in the postData field
117 // in the $_POST to the Shindig proxy server
118 // Here we parse it and put it into the $postDataParams array which then is merged into the postParams
119 // to be used for the GET/POST request and the building of the signature
120 $postDataParams = array();
121 if (isset($_POST['postData']) && count($postDataParts = split('&', urldecode($_POST['postData']))) > 0) {
122 foreach ($postDataParts as $postDataPart) {
123 $position = strpos($postDataPart, '=');
124 $key = substr($postDataPart, 0, $position);
125 $value = substr($postDataPart, $position +
1);
126 $postDataParams[$key] = $value;
129 $postParams = array_merge($postParams, $this->sanitize($postDataParams));
130 $msgParams = array();
131 $msgParams = array_merge($msgParams, $queryParams);
132 $msgParams = array_merge($msgParams, $postParams);
133 $this->addOpenSocialParams($msgParams);
134 $this->addOAuthParams($msgParams);
135 $consumer = new OAuthConsumer(NULL, NULL, NULL);
136 $consumer->setProperty(OAuthSignatureMethod_RSA_SHA1
::$PRIVATE_KEY, $this->privateKeyObject
);
137 $signatureMethod = new OAuthSignatureMethod_RSA_SHA1();
138 $req_req = OAuthRequest
::from_consumer_and_token($consumer, NULL, $method, $resource, $msgParams);
139 $req_req->sign_request($signatureMethod, $consumer, NULL);
140 // Rebuild the query string, including all of the parameters we added.
141 // We have to be careful not to copy POST parameters into the query.
142 // If post and query parameters share a name, they end up being removed
146 if ($method == 'POST') {
147 foreach ($postParams as $key => $param) {
148 $forPost[$key] = $param;
149 if ($postData === false) {
152 $postData[] = OAuthUtil
::urlencodeRFC3986($key) . "=" . OAuthUtil
::urlencodeRFC3986($param);
154 if ($postData !== false) {
155 $postData = implode("&", $postData);
159 foreach ($req_req->get_parameters() as $key => $param) {
160 if (! isset($forPost[$key])) {
161 $newQuery .= urlencode($key) . '=' . urlencode($param) . '&';
164 // and stick on the original query params too
165 if (isset($parsedUri['query']) && ! empty($parsedUri['query'])) {
167 parse_str($parsedUri['query'], $oldQuery);
168 foreach ($oldQuery as $key => $val) {
169 $newQuery .= urlencode($key) . '=' . urlencode($val) . '&';
172 // Careful here; the OAuth form encoding scheme is slightly different than
173 // the normal form encoding scheme, so we have to use the OAuth library
174 // formEncode method.
175 $url = $parsedUri['scheme'] . '://' . $parsedUri['host'] . (isset($parsedUri['port']) ?
':' . $parsedUri['port'] : '') . $parsedUri['path'] . '?' . $newQuery;
176 // The headers are transmitted in the POST-data array in the field 'headers'
177 // if no post should be made, the value should be false for this parameter
178 $postHeaders = ((isset($_POST['headers']) && $method == 'POST') ?
$_POST['headers'] : false);
179 return new RemoteContentRequest($url, $postHeaders, $postData);
180 } catch (Exception
$e) {
181 throw new GadgetException($e);
185 private function addOpenSocialParams(&$msgParams) {
186 $owner = $this->authToken
->getOwnerId();
187 if ($owner != null) {
188 $msgParams[SigningFetcher
::$OPENSOCIAL_OWNERID] = $owner;
190 $viewer = $this->authToken
->getViewerId();
191 if ($viewer != null) {
192 $msgParams[SigningFetcher
::$OPENSOCIAL_VIEWERID] = $viewer;
194 $app = $this->authToken
->getAppId();
196 $msgParams[SigningFetcher
::$OPENSOCIAL_APPID] = $app;
200 private function addOAuthParams(&$msgParams) {
201 $msgParams[OAuth
::$OAUTH_TOKEN] = '';
202 $domain = $this->authToken
->getDomain();
203 if ($domain != null) {
204 $msgParams[OAuth
::$OAUTH_CONSUMER_KEY] = $domain;
206 if ($this->keyName
!= null) {
207 $msgParams[SigningFetcher
::$XOAUTH_PUBLIC_KEY] = $this->keyName
;
209 $nonce = OAuthRequest
::generate_nonce();
210 $msgParams[OAuth
::$OAUTH_NONCE] = $nonce;
212 $msgParams[OAuth
::$OAUTH_TIMESTAMP] = $timestamp;
213 $msgParams[OAuth
::$OAUTH_SIGNATURE_METHOD] = OAuth
::$RSA_SHA1;
217 * Strip out any owner or viewer id passed by the client.
219 private function sanitize($params) {
221 foreach ($params as $key => $p) {
222 if ($this->allowParam($key)) {
229 private function allowParam($paramName) {
230 $canonParamName = strtolower($paramName);
231 // Exclude the fields which are only used to tell the proxy what to do
232 // and the fields which should be added by signing the request later on
233 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") {
236 // make a last sanity check on the key of the data by using a regular expression
237 return ereg(SigningFetcher
::$ALLOWED_PARAM_NAME, $canonParamName);