Completed
Pull Request — release-2.1 (#4446)
by Mathias
12:43
created

proxy.php (4 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/**
4
 * This is a lightweight proxy for serving images, generally meant to be used alongside SSL
5
 *
6
 * Simple Machines Forum (SMF)
7
 *
8
 * @package SMF
9
 * @author Simple Machines http://www.simplemachines.org
10
 * @copyright 2018 Simple Machines and individual contributors
11
 * @license http://www.simplemachines.org/about/smf/license.php BSD
12
 *
13
 * @version 2.1 Beta 4
14
 */
15
16
define('SMF', 'proxy');
17
18
/**
19
 * Class ProxyServer
20
 */
21
class ProxyServer
22
{
23
	/** @var bool $enabled Whether or not this is enabled */
24
	protected $enabled;
25
26
	/** @var int $maxSize The maximum size for files to cache */
27
	protected $maxSize;
28
29
	/** @var string $secret A secret code used for hashing */
30
	protected $secret;
31
32
	/** @var string The cache directory */
33
	protected $cache;
34
35
	/**
36
	 * Constructor, loads up the Settings for the proxy
37
	 *
38
	 * @access public
39
	 */
40
	public function __construct()
41
	{
42
		global $image_proxy_enabled, $image_proxy_maxsize, $image_proxy_secret, $cachedir, $sourcedir;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
43
44
		require_once(dirname(__FILE__) . '/Settings.php');
45
		require_once($sourcedir . '/Class-CurlFetchWeb.php');
46
		require_once($sourcedir . '/Subs.php');
47
48
49
		// Turn off all error reporting; any extra junk makes for an invalid image.
50
		error_reporting(0);
51
52
		$this->enabled = (bool) $image_proxy_enabled;
53
		$this->maxSize = (int) $image_proxy_maxsize;
54
		$this->secret = (string) $image_proxy_secret;
55
		$this->cache = $cachedir . '/images';
56
	}
57
58
	/**
59
	 * Checks whether the request is valid or not
60
	 *
61
	 * @access public
62
	 * @return bool Whether the request is valid
0 ignored issues
show
Should the return type not be integer|null|boolean?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
63
	 */
64
	public function checkRequest()
65
	{
66
		if (!$this->enabled)
67
			return false;
68
69
		// Try to create the image cache directory if it doesn't exist
70
		if (!file_exists($this->cache))
71
			if (!mkdir($this->cache) || !copy(dirname($this->cache) . '/index.php', $this->cache . '/index.php'))
72
				return false;
73
74
		if (empty($_GET['hash']) || empty($_GET['request']))
75
			return false;
76
77
		$hash = $_GET['hash'];
78
		$request = $_GET['request'];
79
80
		if (md5($request . $this->secret) != $hash)
81
			return false;
82
83
		// Attempt to cache the request if it doesn't exist
84
		if (!$this->isCached($request))
85
			return $this->cacheImage($request);
86
87
		return true;
88
	}
89
90
	/**
91
	 * Serves the request
92
	 *
93
	 * @access public
94
	 * @return void
95
	 */
96
	public function serve()
97
	{
98
		$request = $_GET['request'];
99
		$cached_file = $this->getCachedPath($request);
100
		$cached = json_decode(file_get_contents($cached_file), true);
101
102
		// Did we get an error when trying to fetch the image
103
		$response = $this->checkRequest();
104
		if (!$response) {
105
			// Throw a 404
106
			header('HTTP/1.0 404 Not Found');
107
			exit;
108
		}
109
110
		// Is the cache expired?
111
		if (!$cached || time() - $cached['time'] > (5 * 86400))
112
		{
113
			@unlink($cached_file);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
114
			if ($this->checkRequest())
115
				$this->serve();
116
			redirectexit($request);
117
		}
118
119
		// Right, image not cached? Simply redirect, then.
120
		if (!$response)
121
		    redirectexit($request);
122
123
		// Make sure we're serving an image
124
		$contentParts = explode('/', !empty($cached['content_type']) ? $cached['content_type'] : '');
125
		if ($contentParts[0] != 'image')
126
			exit;
127
128
		header('Content-type: ' . $cached['content_type']);
129
		header('Content-length: ' . $cached['size']);
130
		echo base64_decode($cached['body']);
131
	}
132
133
	/**
134
	 * Returns the request's hashed filepath
135
	 *
136
	 * @access public
137
	 * @param string $request The request to get the path for
138
	 * @return string The hashed filepath for the specified request
139
	 */
140
	protected function getCachedPath($request)
141
	{
142
		return $this->cache . '/' . sha1($request . $this->secret);
143
	}
144
145
	/**
146
	 * Check whether the image exists in local cache or not
147
	 *
148
	 * @access protected
149
	 * @param string $request The image to check for in the cache
150
	 * @return bool Whether or not the requested image is cached
151
	 */
152
	protected function isCached($request)
153
	{
154
		return file_exists($this->getCachedPath($request));
155
	}
156
157
	/**
158
	 * Attempts to cache the image while validating it
159
	 *
160
	 * @access protected
161
	 * @param string $request The image to cache/validate
162
	 * @return bool|int Whether the specified image was cached or error code when accessing
0 ignored issues
show
Should the return type not be false|integer|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
163
	 */
164
	protected function cacheImage($request)
165
	{
166
		$dest = $this->getCachedPath($request);
167
168
		$curl = new curl_fetch_web_data(array(CURLOPT_BINARYTRANSFER => 1));
169
		$request = $curl->get_url_data($request);
170
		$responseCode = $request->result('code');
171
		$response = $request->result();
172
173
		if (empty($response))
174
			return false;
175
176
		if ($responseCode != 200) {
177
			return false;
178
		}
179
180
		$headers = $response['headers'];
181
182
		// Make sure the url is returning an image
183
		$contentParts = explode('/', !empty($headers['content-type']) ? $headers['content-type'] : '');
184
		if ($contentParts[0] != 'image')
185
			return false;
186
187
		// Validate the filesize
188
		if ($response['size'] > ($this->maxSize * 1024))
189
			return false;
190
191
		return file_put_contents($dest, json_encode(array(
192
			'content_type' => $headers['content-type'],
193
			'size' => $response['size'],
194
			'time' => time(),
195
			'body' => base64_encode($response['body']),
196
		))) === false ? 1 : null;
197
	}
198
}
199
200
$proxy = new ProxyServer();
201
$proxy->serve();
202
203
?>