1 <?php
2
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
24 class rex_socket
25 {
26 protected $host;
27 protected $port;
28 protected $ssl;
29 protected $path = '/';
30 protected $timeout = 15;
31 protected $followRedirects = false;
32 protected = [];
33 protected $stream;
34
35 36 37 38 39 40 41
42 protected function __construct($host, $port = 80, $ssl = false)
43 {
44 $this->host = $host;
45 $this->port = $port;
46 $this->ssl = $ssl;
47
48 $this->addHeader('Host', $this->host);
49 $this->addHeader('User-Agent', 'REDAXO/' . rex::getVersion());
50 $this->addHeader('Connection', 'Close');
51 }
52
53 54 55 56 57 58 59 60 61 62 63
64 public static function factory($host, $port = 80, $ssl = false)
65 {
66 if (static::class === self::class && ($proxy = rex::getProperty('socket_proxy'))) {
67 return rex_socket_proxy::factoryUrl($proxy)->setDestination($host, $port, $ssl);
68 }
69
70 return new static($host, $port, $ssl);
71 }
72
73 74 75 76 77 78 79 80 81 82 83
84 public static function factoryUrl($url)
85 {
86 $parts = self::parseUrl($url);
87
88 return static::factory($parts['host'], $parts['port'], $parts['ssl'])->setPath($parts['path']);
89 }
90
91 92 93 94 95 96 97
98 public function setPath($path)
99 {
100 $this->path = $path;
101
102 return $this;
103 }
104
105 106 107 108 109 110 111 112
113 public function ($key, $value)
114 {
115 $this->headers[$key] = $value;
116
117 return $this;
118 }
119
120 121 122 123 124 125 126 127
128 public function addBasicAuthorization($user, $password)
129 {
130 $this->addHeader('Authorization', 'Basic ' . base64_encode($user . ':' . $password));
131
132 return $this;
133 }
134
135 136 137 138 139 140 141
142 public function setTimeout($timeout)
143 {
144 $this->timeout = $timeout;
145
146 return $this;
147 }
148
149 150 151 152 153 154 155 156 157
158 public function followRedirects($redirects)
159 {
160 if ($redirects < 0) {
161 throw new InvalidArgumentException(sprintf('$redirects must be `null` or an int >= 0, given "%s".', $redirects));
162 }
163
164 $this->followRedirects = $redirects;
165
166 return $this;
167 }
168
169 170 171 172 173 174 175
176 public function doGet()
177 {
178 return $this->doRequest('GET');
179 }
180
181 182 183 184 185 186 187 188 189 190
191 public function doPost($data = '', array $files = [])
192 {
193 if (is_array($data) && !empty($files)) {
194 $data = function ($stream) use ($data, $files) {
195 $boundary = '----------6n2Yd9bk2liD6piRHb5xF6';
196 $eol = "\r\n";
197 fwrite($stream, 'Content-Type: multipart/form-data; boundary=' . $boundary . $eol);
198 $dataFormat = '--' . $boundary . $eol . 'Content-Disposition: form-data; name="%s"' . $eol . $eol;
199 $fileFormat = '--' . $boundary . $eol . 'Content-Disposition: form-data; name="%s"; filename="%s"' . $eol . 'Content-Type: %s' . $eol . $eol;
200 $end = '--' . $boundary . '--' . $eol;
201 $length = 0;
202 $temp = explode('&', rex_string::buildQuery($data));
203 $data = [];
204 $partLength = rex_string::size(sprintf($dataFormat, '') . $eol);
205 foreach ($temp as $t) {
206 list($key, $value) = array_map('urldecode', explode('=', $t, 2));
207 $data[$key] = $value;
208 $length += $partLength + rex_string::size($key) + rex_string::size($value);
209 }
210 $partLength = rex_string::size(sprintf($fileFormat, '', '', '') . $eol);
211 foreach ($files as $key => $file) {
212 $length += $partLength + rex_string::size($key) + rex_string::size(basename($file['path'])) + rex_string::size($file['type']) + filesize($file['path']);
213 }
214 $length += rex_string::size($end);
215 fwrite($stream, 'Content-Length: ' . $length . $eol . $eol);
216 foreach ($data as $key => $value) {
217 fwrite($stream, sprintf($dataFormat, $key) . $value . $eol);
218 }
219 foreach ($files as $key => $file) {
220 fwrite($stream, sprintf($fileFormat, $key, basename($file['path']), $file['type']));
221 $file = fopen($file['path'], 'r');
222 while (!feof($file)) {
223 fwrite($stream, fread($file, 1024));
224 }
225 fclose($file);
226 fwrite($stream, $eol);
227 }
228 fwrite($stream, $end);
229 };
230 } elseif (!is_callable($data)) {
231 if (is_array($data)) {
232 $data = rex_string::buildQuery($data);
233 $this->addHeader('Content-Type', 'application/x-www-form-urlencoded');
234 }
235 }
236 return $this->doRequest('POST', $data);
237 }
238
239 240 241 242 243 244 245
246 public function doDelete()
247 {
248 return $this->doRequest('DELETE');
249 }
250
251 252 253 254 255 256 257 258 259 260
261 public function doRequest($method, $data = '')
262 {
263 return rex_timer::measure(__METHOD__, function () use ($method, $data) {
264 if (!is_string($data) && !is_callable($data)) {
265 throw new InvalidArgumentException(sprintf('Expecting $data to be a string or a callable, but %s given!', gettype($data)));
266 }
267
268 if (!$this->ssl) {
269 rex_logger::logError(E_WARNING, 'You should not use non-secure socket connections while connecting to "'. $this->host .'"!', __FILE__, __LINE__);
270 }
271
272 $this->openConnection();
273 $response = $this->writeRequest($method, $this->path, $this->headers, $data);
274
275 if ('GET' !== $method || !$this->followRedirects || !$response->isRedirection()) {
276 return $response;
277 }
278
279 $location = $response->getHeader('location');
280
281 if (!$location) {
282 return $response;
283 }
284
285 if (false === strpos($location, '//')) {
286 $socket = self::factory($this->host, $this->port, $this->ssl)->setPath($location);
287 } else {
288 $socket = self::factoryUrl($location);
289
290 if ($this->ssl && !$socket->ssl) {
291 return $response;
292 }
293 }
294
295 $socket->setTimeout($this->timeout);
296 $socket->followRedirects($this->followRedirects - 1);
297
298 foreach ($this->headers as $key => $value) {
299 if ('Host' !== $key) {
300 $socket->addHeader($key, $value);
301 }
302 }
303
304 return $socket->doGet();
305 });
306 }
307
308 309 310 311 312
313 protected function openConnection()
314 {
315 $host = ($this->ssl ? 'ssl://' : '') . $this->host;
316
317 $prevError = null;
318 set_error_handler(function ($errno, $errstr) use (&$prevError) {
319 if (null === $prevError) {
320 $prevError = $errstr;
321 }
322 });
323
324 try {
325 $this->stream = @fsockopen($host, $this->port, $errno, $errstr);
326 } finally {
327 restore_error_handler();
328 }
329
330 if ($this->stream) {
331 stream_set_timeout($this->stream, $this->timeout);
332
333 return;
334 }
335
336 if ($errstr) {
337 throw new rex_socket_exception($errstr . ' (' . $errno . ')');
338 }
339
340 if ($prevError) {
341 throw new rex_socket_exception($prevError);
342 }
343
344 throw new rex_socket_exception('Unknown error.');
345 }
346
347 348 349 350 351 352 353 354 355 356 357 358
359 protected function writeRequest($method, $path, array $headers = [], $data = '')
360 {
361 $eol = "\r\n";
362 $headerStrings = [];
363 $headerStrings[] = strtoupper($method) . ' ' . $path . ' HTTP/1.1';
364 foreach ($headers as $key => $value) {
365 $headerStrings[] = $key . ': ' . $value;
366 }
367 foreach ($headerStrings as $header) {
368 fwrite($this->stream, str_replace(["\r", "\n"], '', $header) . $eol);
369 }
370 if (!is_callable($data)) {
371 fwrite($this->stream, 'Content-Length: ' . rex_string::size($data) . $eol);
372 fwrite($this->stream, $eol . $data);
373 } else {
374 call_user_func($data, $this->stream);
375 }
376
377 $meta = stream_get_meta_data($this->stream);
378 if (isset($meta['timed_out']) && $meta['timed_out']) {
379 throw new rex_socket_exception('Timeout!');
380 }
381
382 return new rex_socket_response($this->stream);
383 }
384
385 386 387 388 389 390 391 392 393
394 protected static function parseUrl($url)
395 {
396 $parts = parse_url($url);
397 if ($parts !== false && !isset($parts['host']) && strpos($url, 'http') !== 0) {
398 $parts = parse_url('http://' . $url);
399 }
400 if ($parts === false || !isset($parts['host'])) {
401 throw new rex_socket_exception('It isn\'t possible to parse the URL "' . $url . '"!');
402 }
403
404 $port = 80;
405 $ssl = false;
406 if (isset($parts['scheme'])) {
407 $supportedProtocols = ['http', 'https'];
408 if (!in_array($parts['scheme'], $supportedProtocols)) {
409 throw new rex_socket_exception('Unsupported protocol "' . $parts['scheme'] . '". Supported protocols are ' . implode(', ', $supportedProtocols) . '.');
410 }
411 if ($parts['scheme'] == 'https') {
412 $ssl = true;
413 $port = 443;
414 }
415 }
416 $port = isset($parts['port']) ? (int) $parts['port'] : $port;
417
418 $path = (isset($parts['path']) ? $parts['path'] : '/')
419 . (isset($parts['query']) ? '?' . $parts['query'] : '')
420 . (isset($parts['fragment']) ? '#' . $parts['fragment'] : '');
421
422 return [
423 'host' => $parts['host'],
424 'port' => $port,
425 'ssl' => $ssl,
426 'path' => $path,
427 ];
428 }
429 }
430
431 432 433 434 435 436 437
438 class rex_socket_exception extends rex_exception
439 {
440 }
441