1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13:
14: namespace Cake\Http\Client\Adapter;
15:
16: use Cake\Core\Exception\Exception;
17: use Cake\Http\Client\AdapterInterface;
18: use Cake\Http\Client\Request;
19: use Cake\Http\Client\Response;
20: use Cake\Http\Exception\HttpException;
21:
22: 23: 24: 25: 26: 27:
28: class Stream implements AdapterInterface
29: {
30: 31: 32: 33: 34:
35: protected $_context;
36:
37: 38: 39: 40: 41:
42: protected $_contextOptions;
43:
44: 45: 46: 47: 48:
49: protected $_sslContextOptions;
50:
51: 52: 53: 54: 55:
56: protected $_stream;
57:
58: 59: 60: 61: 62:
63: protected $_connectionErrors = [];
64:
65: 66: 67:
68: public function send(Request $request, array $options)
69: {
70: $this->_stream = null;
71: $this->_context = null;
72: $this->_contextOptions = [];
73: $this->_sslContextOptions = [];
74: $this->_connectionErrors = [];
75:
76: $this->_buildContext($request, $options);
77:
78: return $this->_send($request);
79: }
80:
81: 82: 83: 84: 85: 86: 87: 88: 89: 90:
91: public function createResponses($headers, $content)
92: {
93: $indexes = $responses = [];
94: foreach ($headers as $i => $header) {
95: if (strtoupper(substr($header, 0, 5)) === 'HTTP/') {
96: $indexes[] = $i;
97: }
98: }
99: $last = count($indexes) - 1;
100: foreach ($indexes as $i => $start) {
101: $end = isset($indexes[$i + 1]) ? $indexes[$i + 1] - $start : null;
102: $headerSlice = array_slice($headers, $start, $end);
103: $body = $i == $last ? $content : '';
104: $responses[] = $this->_buildResponse($headerSlice, $body);
105: }
106:
107: return $responses;
108: }
109:
110: 111: 112: 113: 114: 115: 116:
117: protected function _buildContext(Request $request, $options)
118: {
119: $this->_buildContent($request, $options);
120: $this->_buildHeaders($request, $options);
121: $this->_buildOptions($request, $options);
122:
123: $url = $request->getUri();
124: $scheme = parse_url($url, PHP_URL_SCHEME);
125: if ($scheme === 'https') {
126: $this->_buildSslContext($request, $options);
127: }
128: $this->_context = stream_context_create([
129: 'http' => $this->_contextOptions,
130: 'ssl' => $this->_sslContextOptions,
131: ]);
132: }
133:
134: 135: 136: 137: 138: 139: 140: 141: 142:
143: protected function _buildHeaders(Request $request, $options)
144: {
145: $headers = [];
146: foreach ($request->getHeaders() as $name => $values) {
147: $headers[] = sprintf('%s: %s', $name, implode(', ', $values));
148: }
149: $this->_contextOptions['header'] = implode("\r\n", $headers);
150: }
151:
152: 153: 154: 155: 156: 157: 158: 159: 160: 161:
162: protected function _buildContent(Request $request, $options)
163: {
164: $body = $request->getBody();
165: if (empty($body)) {
166: $this->_contextOptions['content'] = '';
167:
168: return;
169: }
170: $body->rewind();
171: $this->_contextOptions['content'] = $body->getContents();
172: }
173:
174: 175: 176: 177: 178: 179: 180:
181: protected function _buildOptions(Request $request, $options)
182: {
183: $this->_contextOptions['method'] = $request->getMethod();
184: $this->_contextOptions['protocol_version'] = $request->getProtocolVersion();
185: $this->_contextOptions['ignore_errors'] = true;
186:
187: if (isset($options['timeout'])) {
188: $this->_contextOptions['timeout'] = $options['timeout'];
189: }
190:
191: $this->_contextOptions['max_redirects'] = 0;
192:
193: if (isset($options['proxy']['proxy'])) {
194: $this->_contextOptions['request_fulluri'] = true;
195: $this->_contextOptions['proxy'] = $options['proxy']['proxy'];
196: }
197: }
198:
199: 200: 201: 202: 203: 204: 205:
206: protected function _buildSslContext(Request $request, $options)
207: {
208: $sslOptions = [
209: 'ssl_verify_peer',
210: 'ssl_verify_peer_name',
211: 'ssl_verify_depth',
212: 'ssl_allow_self_signed',
213: 'ssl_cafile',
214: 'ssl_local_cert',
215: 'ssl_passphrase',
216: ];
217: if (empty($options['ssl_cafile'])) {
218: $options['ssl_cafile'] = CORE_PATH . 'config' . DIRECTORY_SEPARATOR . 'cacert.pem';
219: }
220: if (!empty($options['ssl_verify_host'])) {
221: $url = $request->getUri();
222: $host = parse_url($url, PHP_URL_HOST);
223: $this->_sslContextOptions['peer_name'] = $host;
224: }
225: foreach ($sslOptions as $key) {
226: if (isset($options[$key])) {
227: $name = substr($key, 4);
228: $this->_sslContextOptions[$name] = $options[$key];
229: }
230: }
231: }
232:
233: 234: 235: 236: 237: 238: 239:
240: protected function _send(Request $request)
241: {
242: $deadline = false;
243: if (isset($this->_contextOptions['timeout']) && $this->_contextOptions['timeout'] > 0) {
244: $deadline = time() + $this->_contextOptions['timeout'];
245: }
246:
247: $url = $request->getUri();
248: $this->_open($url);
249: $content = '';
250: $timedOut = false;
251:
252: while (!feof($this->_stream)) {
253: if ($deadline !== false) {
254: stream_set_timeout($this->_stream, max($deadline - time(), 1));
255: }
256:
257: $content .= fread($this->_stream, 8192);
258:
259: $meta = stream_get_meta_data($this->_stream);
260: if ($meta['timed_out'] || ($deadline !== false && time() > $deadline)) {
261: $timedOut = true;
262: break;
263: }
264: }
265: $meta = stream_get_meta_data($this->_stream);
266: fclose($this->_stream);
267:
268: if ($timedOut) {
269: throw new HttpException('Connection timed out ' . $url, 504);
270: }
271:
272: $headers = $meta['wrapper_data'];
273: if (isset($headers['headers']) && is_array($headers['headers'])) {
274: $headers = $headers['headers'];
275: }
276:
277: return $this->createResponses($headers, $content);
278: }
279:
280: 281: 282: 283: 284: 285: 286: 287:
288: protected function _buildResponse($headers, $body)
289: {
290: return new Response($headers, $body);
291: }
292:
293: 294: 295: 296: 297: 298: 299:
300: protected function _open($url)
301: {
302: set_error_handler(function ($code, $message) {
303: $this->_connectionErrors[] = $message;
304: });
305: try {
306: $this->_stream = fopen($url, 'rb', false, $this->_context);
307: } finally {
308: restore_error_handler();
309: }
310:
311: if (!$this->_stream || !empty($this->_connectionErrors)) {
312: throw new Exception(implode("\n", $this->_connectionErrors));
313: }
314: }
315:
316: 317: 318: 319: 320: 321: 322:
323: public function contextOptions()
324: {
325: return array_merge($this->_contextOptions, $this->_sslContextOptions);
326: }
327: }
328:
329:
330: class_alias('Cake\Http\Client\Adapter\Stream', 'Cake\Network\Http\Adapter\Stream');
331: