switch our signed rquests (called from preloads, makeRequest and data-pipelining...
[shindig.git] / php / src / gadgets / SigningFetcher.php
blob98fca6eb47c6fe65dd2debe45a40061affaaeacf
1 <?php
2 /**
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
18 * under the License.
21 /**
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:]]+$';
38 /**
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;
45 /**
46 * The name of the key, included in the fetch to help with key rotation.
48 protected $keyName;
50 /**
51 * @var RemoteContentFetcher
53 private $fetcher;
55 /**
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);
67 /**
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);
79 /**
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();
112 try {
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);
116 $resource = $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()) {
131 if ($signBody) {
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);
136 } else {
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);
144 if ($signBody) {
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
157 // from the query.
158 $forPost = array();
159 $postData = false;
160 if ($method == 'POST' && $signBody) {
161 foreach ($postParams as $key => $param) {
162 $forPost[$key] = $param;
163 if ($postData === false) {
164 $postData = array();
166 $postData[] = OAuthUtil::urlencodeRFC3986($key) . "=" . OAuthUtil::urlencodeRFC3986($param);
168 if ($postData !== false) {
169 $postData = implode("&", $postData);
172 $newQuery = '';
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'])) {
180 $oldQuery = array();
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);
191 if ($signBody) {
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();
209 if ($app != null) {
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;
225 $timestamp = time();
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) {
234 $list = array();
235 foreach ($params as $key => $p) {
236 if ($this->allowParam($key)) {
237 $list[$key] = $p;
240 return $list;
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") {
248 return false;
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);