1<?php
2/*
3 * Copyright 2013 Google Inc.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18/**
19 * Http Streams based implementation of Google_IO.
20 *
21 * @author Stuart Langley <slangley@google.com>
22 */
23
24require_once 'Google_CacheParser.php';
25
26class Google_HttpStreamIO extends Google_IO {
27
28  private static $ENTITY_HTTP_METHODS = array("POST" => null, "PUT" => null);
29
30  private static $DEFAULT_HTTP_CONTEXT = array(
31    "follow_location" => 0,
32    "ignore_errors" => 1,
33  );
34
35  private static $DEFAULT_SSL_CONTEXT = array(
36    "verify_peer" => true,
37  );
38
39  /**
40   * Perform an authenticated / signed apiHttpRequest.
41   * This function takes the apiHttpRequest, calls apiAuth->sign on it
42   * (which can modify the request in what ever way fits the auth mechanism)
43   * and then calls Google_HttpStreamIO::makeRequest on the signed request
44   *
45   * @param Google_HttpRequest $request
46   * @return Google_HttpRequest The resulting HTTP response including the
47   * responseHttpCode, responseHeaders and responseBody.
48   */
49  public function authenticatedRequest(Google_HttpRequest $request) {
50    $request = Google_Client::$auth->sign($request);
51    return $this->makeRequest($request);
52  }
53
54  /**
55   * Execute a apiHttpRequest
56   *
57   * @param Google_HttpRequest $request the http request to be executed
58   * @return Google_HttpRequest http request with the response http code,
59   * response headers and response body filled in
60   * @throws Google_IOException on curl or IO error
61   */
62  public function makeRequest(Google_HttpRequest $request) {
63    // First, check to see if we have a valid cached version.
64    $cached = $this->getCachedRequest($request);
65    if ($cached !== false) {
66      if (!$this->checkMustRevaliadateCachedRequest($cached, $request)) {
67        return $cached;
68      }
69    }
70
71    $default_options = stream_context_get_options(stream_context_get_default());
72
73    $requestHttpContext = array_key_exists('http', $default_options) ?
74        $default_options['http'] : array();
75    if (array_key_exists($request->getRequestMethod(),
76          self::$ENTITY_HTTP_METHODS)) {
77      $request = $this->processEntityRequest($request);
78    }
79
80    if ($request->getPostBody()) {
81      $requestHttpContext["content"] = $request->getPostBody();
82    }
83
84    $requestHeaders = $request->getRequestHeaders();
85    if ($requestHeaders && is_array($requestHeaders)) {
86      $headers = "";
87      foreach($requestHeaders as $k => $v) {
88        $headers .= "$k: $v\n";
89      }
90      $requestHttpContext["header"] = $headers;
91    }
92
93    $requestHttpContext["method"] = $request->getRequestMethod();
94    $requestHttpContext["user_agent"] = $request->getUserAgent();
95
96    $requestSslContext = array_key_exists('ssl', $default_options) ?
97        $default_options['ssl'] : array();
98
99    if (!array_key_exists("cafile", $requestSslContext)) {
100      $requestSslContext["cafile"] = dirname(__FILE__) . '/cacerts.pem';
101    }
102
103    $options = array("http" => array_merge(self::$DEFAULT_HTTP_CONTEXT,
104                                                 $requestHttpContext),
105                     "ssl" => array_merge(self::$DEFAULT_SSL_CONTEXT,
106                                          $requestSslContext));
107
108    $context = stream_context_create($options);
109
110    $response_data = file_get_contents($request->getUrl(),
111                                       false,
112                                       $context);
113
114    if (false === $response_data) {
115      throw new Google_IOException("HTTP Error: Unable to connect");
116    }
117
118    $respHttpCode = $this->getHttpResponseCode($http_response_header);
119    $responseHeaders = $this->getHttpResponseHeaders($http_response_header);
120
121    if ($respHttpCode == 304 && $cached) {
122      // If the server responded NOT_MODIFIED, return the cached request.
123      $this->updateCachedRequest($cached, $responseHeaders);
124      return $cached;
125    }
126
127    $request->setResponseHttpCode($respHttpCode);
128    $request->setResponseHeaders($responseHeaders);
129    $request->setResponseBody($response_data);
130    // Store the request in cache (the function checks to see if the request
131    // can actually be cached)
132    $this->setCachedRequest($request);
133    return $request;
134  }
135
136  /**
137   * Set options that update the transport implementation's behavior.
138   * @param $options
139   */
140  public function setOptions($options) {
141  }
142
143  private function getHttpResponseCode($response_headers) {
144    $header_count = count($response_headers);
145
146    for ($i = 0; $i < $header_count; $i++) {
147      $header = $response_headers[$i];
148      if (strncasecmp("HTTP", $header, strlen("HTTP")) == 0) {
149        $response = explode(' ', $header);
150        return $response[1];
151      }
152    }
153    return 'UNKNOWN';
154  }
155
156  private function getHttpResponseHeaders($response_headers) {
157    $header_count = count($response_headers);
158    $headers = array();
159
160    for ($i = 0; $i < $header_count; $i++) {
161      $header = $response_headers[$i];
162      $header_parts = explode(':', $header);
163      if (count($header_parts) == 2) {
164        $headers[$header_parts[0]] = $header_parts[1];
165      }
166    }
167
168    return $headers;
169  }
170}
171