Completed
Push — release-2.1 ( 62bce6...b11117 )
by Colin
08:13
created

proxy.php (2 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;
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);
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
?>