Teknik is a suite of services with attractive and functional interfaces. https://www.teknik.io/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ProxyHttp.php 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik;
  10. /**
  11. * Http helper: static file server proxy, with compression, caching, isHttps() helper...
  12. *
  13. * Used to server piwik.js and the merged+minified CSS and JS files
  14. *
  15. */
  16. class ProxyHttp
  17. {
  18. const DEFLATE_ENCODING_REGEX = '/(?:^|, ?)(deflate)(?:,|$)/';
  19. const GZIP_ENCODING_REGEX = '/(?:^|, ?)((x-)?gzip)(?:,|$)/';
  20. /**
  21. * Returns true if the current request appears to be a secure HTTPS connection
  22. *
  23. * @return bool
  24. */
  25. public static function isHttps()
  26. {
  27. return Url::getCurrentScheme() === 'https';
  28. }
  29. /**
  30. * Serve static files through php proxy.
  31. *
  32. * It performs the following actions:
  33. * - Checks the file is readable or returns "HTTP/1.0 404 Not Found"
  34. * - Returns "HTTP/1.1 304 Not Modified" after comparing the HTTP_IF_MODIFIED_SINCE
  35. * with the modification date of the static file
  36. * - Will try to compress the static file according to HTTP_ACCEPT_ENCODING. Compressed files are store in
  37. * the /tmp directory. If compressing extensions are not available, a manually gzip compressed file
  38. * can be provided in the /tmp directory. It has to bear the same name with an added .gz extension.
  39. * Using manually compressed static files requires you to manually update the compressed file when
  40. * the static file is updated.
  41. * - Overrides server cache control config to allow caching
  42. * - Sends Very Accept-Encoding to tell proxies to store different version of the static file according
  43. * to users encoding capacities.
  44. *
  45. * Warning:
  46. * Compressed filed are stored in the /tmp directory.
  47. * If this method is used with two files bearing the same name but located in different locations,
  48. * there is a risk of conflict. One file could be served with the content of the other.
  49. * A future upgrade of this method would be to recreate the directory structure of the static file
  50. * within a /tmp/compressed-static-files directory.
  51. *
  52. * @param string $file The location of the static file to serve
  53. * @param string $contentType The content type of the static file.
  54. * @param bool $expireFarFuture Day in the far future to set the Expires header to.
  55. * Should be set to false for files that should not be cached.
  56. * @param int|false $byteStart The starting byte in the file to serve. If false, the data from the beginning
  57. * of the file will be served.
  58. * @param int|false $byteEnd The ending byte in the file to serve. If false, the data from $byteStart to the
  59. * end of the file will be served.
  60. */
  61. public static function serverStaticFile($file, $contentType, $expireFarFutureDays = 100, $byteStart = false,
  62. $byteEnd = false)
  63. {
  64. // if the file cannot be found return HTTP status code '404'
  65. if (!file_exists($file)) {
  66. self::setHttpStatus('404 Not Found');
  67. return;
  68. }
  69. $modifiedSince = Http::getModifiedSinceHeader();
  70. $fileModifiedTime = @filemtime($file);
  71. $lastModified = gmdate('D, d M Y H:i:s', $fileModifiedTime) . ' GMT';
  72. // set some HTTP response headers
  73. self::overrideCacheControlHeaders('public');
  74. Common::sendHeader('Vary: Accept-Encoding');
  75. Common::sendHeader('Content-Disposition: inline; filename=' . basename($file));
  76. if ($expireFarFutureDays) {
  77. // Required by proxy caches potentially in between the browser and server to cache the request indeed
  78. Common::sendHeader(self::getExpiresHeaderForFutureDay($expireFarFutureDays));
  79. }
  80. // Return 304 if the file has not modified since
  81. if ($modifiedSince === $lastModified) {
  82. self::setHttpStatus('304 Not Modified');
  83. return;
  84. }
  85. // if we have to serve the file, serve it now, either in the clear or compressed
  86. if ($byteStart === false) {
  87. $byteStart = 0;
  88. }
  89. if ($byteEnd === false) {
  90. $byteEnd = filesize($file);
  91. }
  92. $compressed = false;
  93. $encoding = '';
  94. $compressedFileLocation = AssetManager::getInstance()->getAssetDirectory() . '/' . basename($file);
  95. if (!($byteStart == 0
  96. && $byteEnd == filesize($file))
  97. ) {
  98. $compressedFileLocation .= ".$byteStart.$byteEnd";
  99. }
  100. $phpOutputCompressionEnabled = self::isPhpOutputCompressed();
  101. if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && !$phpOutputCompressionEnabled) {
  102. list($encoding, $extension) = self::getCompressionEncodingAcceptedByClient();
  103. $filegz = $compressedFileLocation . $extension;
  104. if (self::canCompressInPhp()) {
  105. if (!empty($encoding)) {
  106. // compress the file if it doesn't exist or is newer than the existing cached file, and cache
  107. // the compressed result
  108. if (self::shouldCompressFile($file, $filegz)) {
  109. self::compressFile($file, $filegz, $encoding, $byteStart, $byteEnd);
  110. }
  111. $compressed = true;
  112. $file = $filegz;
  113. $byteStart = 0;
  114. $byteEnd = filesize($file);
  115. }
  116. } else {
  117. // if a compressed file exists, the file was manually compressed so we just serve that
  118. if ($extension == '.gz'
  119. && !self::shouldCompressFile($file, $filegz)
  120. ) {
  121. $compressed = true;
  122. $file = $filegz;
  123. $byteStart = 0;
  124. $byteEnd = filesize($file);
  125. }
  126. }
  127. }
  128. Common::sendHeader('Last-Modified: ' . $lastModified);
  129. if (!$phpOutputCompressionEnabled) {
  130. Common::sendHeader('Content-Length: ' . ($byteEnd - $byteStart));
  131. }
  132. if (!empty($contentType)) {
  133. Common::sendHeader('Content-Type: ' . $contentType);
  134. }
  135. if ($compressed) {
  136. Common::sendHeader('Content-Encoding: ' . $encoding);
  137. }
  138. if (!_readfile($file, $byteStart, $byteEnd)) {
  139. self::setHttpStatus('505 Internal server error');
  140. }
  141. }
  142. /**
  143. * Test if php output is compressed
  144. *
  145. * @return bool True if php output is (or suspected/likely) to be compressed
  146. */
  147. public static function isPhpOutputCompressed()
  148. {
  149. // Off = ''; On = '1'; otherwise, it's a buffer size
  150. $zlibOutputCompression = ini_get('zlib.output_compression');
  151. // could be ob_gzhandler, ob_deflatehandler, etc
  152. $outputHandler = ini_get('output_handler');
  153. // output handlers can be stacked
  154. $obHandlers = array_filter(ob_list_handlers(), function ($var) {
  155. return $var !== "default output handler";
  156. });
  157. // user defined handler via wrapper
  158. if (!defined('PIWIK_TEST_MODE')) {
  159. $autoPrependFile = ini_get('auto_prepend_file');
  160. $autoAppendFile = ini_get('auto_append_file');
  161. }
  162. return !empty($zlibOutputCompression) ||
  163. !empty($outputHandler) ||
  164. !empty($obHandlers) ||
  165. !empty($autoPrependFile) ||
  166. !empty($autoAppendFile);
  167. }
  168. /**
  169. * Workaround IE bug when downloading certain document types over SSL and
  170. * cache control headers are present, e.g.,
  171. *
  172. * Cache-Control: no-cache
  173. * Cache-Control: no-store,max-age=0,must-revalidate
  174. * Pragma: no-cache
  175. *
  176. * @see http://support.microsoft.com/kb/316431/
  177. * @see RFC2616
  178. *
  179. * @param string $override One of "public", "private", "no-cache", or "no-store". (optional)
  180. */
  181. public static function overrideCacheControlHeaders($override = null)
  182. {
  183. if ($override || self::isHttps()) {
  184. Common::sendHeader('Pragma: ');
  185. Common::sendHeader('Expires: ');
  186. if (in_array($override, array('public', 'private', 'no-cache', 'no-store'))) {
  187. Common::sendHeader("Cache-Control: $override, must-revalidate");
  188. } else {
  189. Common::sendHeader('Cache-Control: must-revalidate');
  190. }
  191. }
  192. }
  193. /**
  194. * Set response header, e.g., HTTP/1.0 200 Ok
  195. *
  196. * @param string $status Status
  197. * @return bool
  198. */
  199. protected static function setHttpStatus($status)
  200. {
  201. if (strpos(PHP_SAPI, '-fcgi') === false) {
  202. $key = $_SERVER['SERVER_PROTOCOL'];
  203. } else {
  204. // FastCGI
  205. $key = 'Status:';
  206. }
  207. Common::sendHeader($key . ' ' . $status);
  208. }
  209. /**
  210. * Returns a formatted Expires HTTP header for a certain number of days in the future. The result
  211. * can be used in a call to `header()`.
  212. */
  213. private static function getExpiresHeaderForFutureDay($expireFarFutureDays)
  214. {
  215. return "Expires: " . gmdate('D, d M Y H:i:s', time() + 86400 * (int)$expireFarFutureDays) . ' GMT';
  216. }
  217. private static function getCompressionEncodingAcceptedByClient()
  218. {
  219. $acceptEncoding = $_SERVER['HTTP_ACCEPT_ENCODING'];
  220. if (preg_match(self::DEFLATE_ENCODING_REGEX, $acceptEncoding, $matches)) {
  221. return array('deflate', '.deflate');
  222. } else if (preg_match(self::GZIP_ENCODING_REGEX, $acceptEncoding, $matches)) {
  223. return array('gzip', '.gz');
  224. } else {
  225. return array(false, false);
  226. }
  227. }
  228. private static function canCompressInPhp()
  229. {
  230. return extension_loaded('zlib') && function_exists('file_get_contents') && function_exists('file_put_contents');
  231. }
  232. private static function shouldCompressFile($fileToCompress, $compressedFilePath)
  233. {
  234. $toCompressLastModified = @filemtime($fileToCompress);
  235. $compressedLastModified = @filemtime($compressedFilePath);
  236. return !file_exists($compressedFilePath) || ($toCompressLastModified > $compressedLastModified);
  237. }
  238. private static function compressFile($fileToCompress, $compressedFilePath, $compressionEncoding, $byteStart,
  239. $byteEnd)
  240. {
  241. $data = file_get_contents($fileToCompress);
  242. $data = substr($data, $byteStart, $byteEnd - $byteStart);
  243. if ($compressionEncoding == 'deflate') {
  244. $data = gzdeflate($data, 9);
  245. } else if ($compressionEncoding == 'gzip' || $compressionEncoding == 'x-gzip') {
  246. $data = gzencode($data, 9);
  247. }
  248. file_put_contents($compressedFilePath, $data);
  249. }
  250. }