Completed
Push — release-2.1 ( b11117...7cbb85 )
by Colin
23:04 queued 14:33
created

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
global $proxyhousekeeping;
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...
19
20
/**
21
 * Class ProxyServer
22
 */
23
class ProxyServer
24
{
25
	/** @var bool $enabled Whether or not this is enabled */
26
	protected $enabled;
27
28
	/** @var int $maxSize The maximum size for files to cache */
29
	protected $maxSize;
30
31
	/** @var string $secret A secret code used for hashing */
32
	protected $secret;
33
34
	/** @var string The cache directory */
35
	protected $cache;
36
	
37
	/** @var int $maxDays until enties get deleted */
38
	protected $maxDays;
39
40
	/** @var int time() value */
41
	protected $time;
42
43
	/**
44
	 * Constructor, loads up the Settings for the proxy
45
	 *
46
	 * @access public
47
	 */
48
	public function __construct()
49
	{
50
		global $image_proxy_enabled, $image_proxy_maxsize, $image_proxy_secret, $cachedir, $sourcedir;
51
52
		require_once(dirname(__FILE__) . '/Settings.php');
53
		require_once($sourcedir . '/Class-CurlFetchWeb.php');
54
55
		// Turn off all error reporting; any extra junk makes for an invalid image.
56
		error_reporting(0);
57
58
		$this->enabled = (bool) $image_proxy_enabled;
59
		$this->maxSize = (int) $image_proxy_maxsize;
60
		$this->secret = (string) $image_proxy_secret;
61
		$this->cache = $cachedir . '/images';
62
		$this->maxDays = 5;
63
	}
64
65
	/**
66
	 * Checks whether the request is valid or not
67
	 *
68
	 * @access public
69
	 * @return bool Whether the request is valid
70
	 */
71
	public function checkRequest()
72
	{
73
		if (!$this->enabled)
74
			return false;
75
76
		// Try to create the image cache directory if it doesn't exist
77
		if (!file_exists($this->cache))
78
			if (!mkdir($this->cache) || !copy(dirname($this->cache) . '/index.php', $this->cache . '/index.php'))
79
				return false;
80
81
		if (empty($_GET['hash']) || empty($_GET['request']) || ($_GET['request'] === "http:") || ($_GET['request'] === "https:"))
82
			return false;
83
84
		$hash = $_GET['hash'];
85
		$request = $_GET['request'];
86
87
		if (md5($request . $this->secret) != $hash)
88
			return false;
89
90
		// Attempt to cache the request if it doesn't exist
91
		if (!$this->isCached($request))
92
			return $this->cacheImage($request);
93
94
		return true;
95
	}
96
97
	/**
98
	 * Serves the request
99
	 *
100
	 * @access public
101
	 * @return void
102
	 */
103
	public function serve()
104
	{
105
		$request = $_GET['request'];
106
		$cached_file = $this->getCachedPath($request);
107
		$cached = json_decode(file_get_contents($cached_file), true);
108
109
		// Did we get an error when trying to fetch the image
110
		$response = $this->checkRequest();
111
		if ($response === -1)
112
		{
113
			// Throw a 404
114
			header('HTTP/1.0 404 Not Found');
115
			exit;
116
		}
117
		// Right, image not cached? Simply redirect, then.
118
		if ($response === 0)
119
		{
120
			$this::redirectexit($request);
121
		}
122
123
		$time = $this->getTime();
124
125
		// Is the cache expired?
126
		if (!$cached || time() - $cached['time'] > ($this->maxDays * 86400))
127
		{
128
			@unlink($cached_file);
129
			if ($this->checkRequest())
130
				$this->serve();
131
			$this::redirectexit($request);
132
		}
133
134
		$eTag = '"' . substr(sha1($request) . $cached['time'], 0, 64) . '"';
135 View Code Duplication
		if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) && strpos($_SERVER['HTTP_IF_NONE_MATCH'], $eTag) !== false)
136
		{
137
			header('HTTP/1.1 304 Not Modified');
138
			exit;
139
		}
140
141
		// Make sure we're serving an image
142
		$contentParts = explode('/', !empty($cached['content_type']) ? $cached['content_type'] : '');
143
		if ($contentParts[0] != 'image')
144
			exit;
145
146
		$max_age = $time - $cached['time'] + (5 * 86400);
147
		header('content-type: ' . $cached['content_type']);
148
		header('content-length: ' . $cached['size']);
149
		header('cache-control: public, max-age=' . $max_age );
150
		header('last-modified: ' . gmdate('D, d M Y H:i:s', $cached['time']) . ' GMT');
151
		header('etag: ' . $eTag);
152
		echo base64_decode($cached['body']);
153
	}
154
155
	/**
156
	 * Returns the request's hashed filepath
157
	 *
158
	 * @access public
159
	 * @param string $request The request to get the path for
160
	 * @return string The hashed filepath for the specified request
161
	 */
162
	protected function getCachedPath($request)
163
	{
164
		return $this->cache . '/' . sha1($request . $this->secret);
165
	}
166
167
	/**
168
	 * Check whether the image exists in local cache or not
169
	 *
170
	 * @access protected
171
	 * @param string $request The image to check for in the cache
172
	 * @return bool Whether or not the requested image is cached
173
	 */
174
	protected function isCached($request)
175
	{
176
		return file_exists($this->getCachedPath($request));
177
	}
178
179
	/**
180
	 * Attempts to cache the image while validating it
181
	 *
182
	 * @access protected
183
	 * @param string $request The image to cache/validate
184
	 * @return int -1 error, 0 too big, 1 valid image
185
	 */
186
	protected function cacheImage($request)
187
	{
188
		$dest = $this->getCachedPath($request);
189
		$curl = new curl_fetch_web_data(array(CURLOPT_BINARYTRANSFER => 1));
190
		$curl_request = $curl->get_url_data($request);
191
		$responseCode = $curl_request->result('code');
192
		$response = $curl_request->result();
193
194
		if (empty($response) || $responseCode != 200)
195
		{
196
			return -1;
197
		}
198
199
		$headers = $response['headers'];
200
201
		// Make sure the url is returning an image
202
		$contentParts = explode('/', !empty($headers['content-type']) ? $headers['content-type'] : '');
203
		if ($contentParts[0] != 'image')
204
			return -1;
205
206
		// Validate the filesize
207
		if ($response['size'] > ($this->maxSize * 1024))
208
			return 0;
209
210
		$time = $this->getTime();
211
212
		return file_put_contents($dest, json_encode(array(
213
			'content_type' => $headers['content-type'],
214
			'size' => $response['size'],
215
			'time' => $time,
216
			'body' => base64_encode($response['body']),
217
		))) === false ? -1 : 1; 
218
	}
219
220
	/**
221
	 * Static helper function to redirect a request
222
	 * 
223
	 * @access public
224
	 * @param type $request
225
	 * @return void
226
	 */
227
	static public function redirectexit($request)
228
	{
229
		header('Location: ' . $request, false, 301);
230
		exit;
231
	}
232
233
	/**
234
	 * Helper function to call time() once with the right logic
235
	 * 
236
	 * @return int
237
	 */
238
	protected function getTime()
239
	{
240
		if (empty($this->time))
241
		{
242
			$old_timezone = date_default_timezone_get();
243
			date_default_timezone_set('GMT');
244
			$this->time = time();
245
			date_default_timezone_set($old_timezone);
246
		}
247
248
		return $this->time;
249
	}
250
251
	/**
252
	 * Delete all old entries
253
	 *
254
	 * @access public
255
	 * @return void
256
	 */
257
	public function housekeeping()
258
	{
259
		$path = $this->cache . '/';
260
		if ($handle = opendir($path)) {
261
262
			while (false !== ($file = readdir($handle)))
263
			{ 
264
				$filelastmodified = filemtime($path . $file);
265
266
				if ((time() - $filelastmodified) > ($this->maxDays * 86400))
267
				{
268
				   unlink($path . $file);
269
				}
270
271
			}
272
273
			closedir($handle); 
274
		}
275
	}
276
}
277
278
if (empty($proxyhousekeeping))
279
{
280
	$proxy = new ProxyServer();
281
	$proxy->serve();
282
}
283
284
?>