Passed
Pull Request — release-2.1 (#5093)
by 01
05:06
created

proxy.php (6 issues)

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
if (!defined('SMF'))
17
	define('SMF', 'proxy');
18
19
global $proxyhousekeeping;
20
21
/**
22
 * Class ProxyServer
23
 */
24
class ProxyServer
25
{
26
	/** @var bool $enabled Whether or not this is enabled */
27
	protected $enabled;
28
29
	/** @var int $maxSize The maximum size for files to cache */
30
	protected $maxSize;
31
32
	/** @var string $secret A secret code used for hashing */
33
	protected $secret;
34
35
	/** @var string The cache directory */
36
	protected $cache;
37
38
	/** @var int $maxDays until enties get deleted */
39
	protected $maxDays;
40
41
	/**
42
	 * Constructor, loads up the Settings for the proxy
43
	 *
44
	 * @access public
45
	 */
46
	public function __construct()
47
	{
48
		global $image_proxy_enabled, $image_proxy_maxsize, $image_proxy_secret, $cachedir, $sourcedir;
49
50
		require_once(dirname(__FILE__) . '/Settings.php');
51
		require_once($sourcedir . '/Subs.php');
52
53
		// Turn off all error reporting; any extra junk makes for an invalid image.
54
		error_reporting(0);
55
56
		$this->enabled = (bool) $image_proxy_enabled;
57
		$this->maxSize = (int) $image_proxy_maxsize;
58
		$this->secret = (string) $image_proxy_secret;
59
		$this->cache = $cachedir . '/images';
60
		$this->maxDays = 5;
61
	}
62
63
	/**
64
	 * Checks whether the request is valid or not
65
	 *
66
	 * @access public
67
	 * @return bool Whether the request is valid
68
	 */
69
	public function checkRequest()
70
	{
71
		if (!$this->enabled)
72
			return false;
73
74
		// Try to create the image cache directory if it doesn't exist
75
		if (!file_exists($this->cache))
76
			if (!mkdir($this->cache) || !copy(dirname($this->cache) . '/index.php', $this->cache . '/index.php'))
77
				return false;
78
79
		// Basic sanity check
80
		$_GET['request'] = validate_iri($_GET['request']);
81
82
		// We aren't going anywhere without these
83
		if (empty($_GET['hash']) || empty($_GET['request']))
84
			return false;
85
86
		$hash = $_GET['hash'];
87
		$request = $_GET['request'];
88
89
		if (md5($request . $this->secret) != $hash)
90
			return false;
91
92
		// Ensure any non-ASCII characters in the URL are encoded correctly
93
		$request = iri_to_url($request);
94
95
		// Attempt to cache the request if it doesn't exist
96
		if (!$this->isCached($request))
97
			return $this->cacheImage($request);
98
99
		return true;
100
	}
101
102
	/**
103
	 * Serves the request
104
	 *
105
	 * @access public
106
	 * @return void
107
	 */
108
	public function serve()
109
	{
110
		$request = $_GET['request'];
111
		$cached_file = $this->getCachedPath($request);
112
		$cached = json_decode(file_get_contents($cached_file), true);
113
114
		// Did we get an error when trying to fetch the image
115
		$response = $this->checkRequest();
116
		if ($response === -1)
0 ignored issues
show
The condition $response === -1 is always false.
Loading history...
117
		{
118
			// Throw a 404
119
			send_http_status(404);
120
			exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
121
		}
122
		// Right, image not cached? Simply redirect, then.
123
		if ($response === 0)
0 ignored issues
show
The condition $response === 0 is always false.
Loading history...
124
		{
125
			$this::redirectexit($request);
126
		}
127
128
		$time = time();
129
130
		// Is the cache expired?
131
		if (!$cached || $time - $cached['time'] > ($this->maxDays * 86400))
132
		{
133
			@unlink($cached_file);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

133
			/** @scrutinizer ignore-unhandled */ @unlink($cached_file);

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...
134
			if ($this->checkRequest())
135
				$this->serve();
136
			$this::redirectexit($request);
137
		}
138
139
		$eTag = '"' . substr(sha1($request) . $cached['time'], 0, 64) . '"';
140
		if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) && strpos($_SERVER['HTTP_IF_NONE_MATCH'], $eTag) !== false)
141
		{
142
			send_http_status(304);
143
			exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
144
		}
145
146
		// Make sure we're serving an image
147
		$contentParts = explode('/', !empty($cached['content_type']) ? $cached['content_type'] : '');
148
		if ($contentParts[0] != 'image')
149
			exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
150
151
		$max_age = $time - $cached['time'] + (5 * 86400);
152
		header('content-type: ' . $cached['content_type']);
153
		header('content-length: ' . $cached['size']);
154
		header('cache-control: public, max-age=' . $max_age );
155
		header('last-modified: ' . gmdate('D, d M Y H:i:s', $cached['time']) . ' GMT');
156
		header('etag: ' . $eTag);
157
		echo base64_decode($cached['body']);
158
	}
159
160
	/**
161
	 * Returns the request's hashed filepath
162
	 *
163
	 * @access public
164
	 * @param string $request The request to get the path for
165
	 * @return string The hashed filepath for the specified request
166
	 */
167
	protected function getCachedPath($request)
168
	{
169
		return $this->cache . '/' . sha1($request . $this->secret);
170
	}
171
172
	/**
173
	 * Check whether the image exists in local cache or not
174
	 *
175
	 * @access protected
176
	 * @param string $request The image to check for in the cache
177
	 * @return bool Whether or not the requested image is cached
178
	 */
179
	protected function isCached($request)
180
	{
181
		return file_exists($this->getCachedPath($request));
182
	}
183
184
	/**
185
	 * Attempts to cache the image while validating it
186
	 *
187
	 * @access protected
188
	 * @param string $request The image to cache/validate
189
	 * @return int -1 error, 0 too big, 1 valid image
190
	 */
191
	protected function cacheImage($request)
192
	{
193
		$dest = $this->getCachedPath($request);
194
		$ext = strtolower(pathinfo(parse_url($request, PHP_URL_PATH), PATHINFO_EXTENSION));
195
196
		$image = fetch_web_data($request);
197
198
		// Looks like nobody was home
199
		if (empty($image))
200
			return -1;
201
202
		// What kind of file did they give us?
203
		$finfo = finfo_open(FILEINFO_MIME_TYPE);
204
		$mime_type = finfo_buffer($finfo, $image);
205
206
		// SVG needs a little extra care
207
		if ($ext == 'svg' && $mime_type == 'text/plain')
208
			$mime_type = 'image/svg+xml';
209
210
		// Make sure the url is returning an image
211
		if (strpos($mime_type, 'image/') !== 0)
212
			return -1;
213
214
		// Validate the filesize
215
		$size = strlen($image);
216
		if ($size > ($this->maxSize * 1024))
217
			return 0;
218
219
		// Cache it for later
220
		return file_put_contents($dest, json_encode(array(
221
			'content_type' => $mime_type,
222
			'size' => $size,
223
			'time' => time(),
224
			'body' => base64_encode($image),
225
		))) === false ? -1 : 1;
226
	}
227
228
	/**
229
	 * Static helper function to redirect a request
230
	 *
231
	 * @access public
232
	 * @param type $request
233
	 * @return void
234
	 */
235
	static public function redirectexit($request)
236
	{
237
		header('Location: ' . $request, false, 301);
238
		exit;
239
	}
240
241
	/**
242
	 * Delete all old entries
243
	 *
244
	 * @access public
245
	 * @return void
246
	 */
247
	public function housekeeping()
248
	{
249
		$path = $this->cache . '/';
250
		if ($handle = opendir($path))
251
		{
252
			while (false !== ($file = readdir($handle)))
253
			{
254
				$filelastmodified = filemtime($path . $file);
255
256
				if ((time() - $filelastmodified) > ($this->maxDays * 86400))
257
				{
258
				   unlink($path . $file);
259
				}
260
			}
261
262
			closedir($handle);
263
		}
264
	}
265
}
266
267
if (empty($proxyhousekeeping))
268
{
269
	$proxy = new ProxyServer();
270
	$proxy->serve();
271
}
272
273
?>