Skip to content
Snippets Groups Projects
Select Git revision
  • da7c8ed46bb642a2f8da718da7182b6fb16f13dc
  • development default protected
  • 1360-apply-blacklist-to-video-and-news-results-2
  • translations
  • master
  • 1352-ai-llm-assistant-functionality
  • 1349-improve-mojeek-language-and-regional-codes
  • 1343-update-dependencies
  • 1335-integrate-takeads-serp
  • 1326-create-an-advertiser-portal
  • 1334-donation-campaign
  • 1332-fix-resultpage-searchbar
  • 1322-exchange-help-pictures-to-the-current-metager-version
  • 1319-integrate-infobox
  • 1320-integrate-maps-view
  • 1308-implement-icons-in-css
  • 1299-restructure-parameter-filters
  • 1281-link-to-contact-form-broken-on-team-page-2
  • 1245-token-changes-require-some-help-file-changes-search-in-search-ceased
  • 1240-remove-some-spelling-mistakes
  • 1235-migrate-to-new-keymanager
21 results

index.blade.php

Blame
  • ProxyController.php 22.54 KiB
    <?php
    
    namespace App\Http\Controllers;
    
    use App\CssDocument;
    use App\HtmlDocument;
    use Cache;
    use finfo;
    use Log;
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\Redis;
    use App\Console\Commands\RequestFetcher;
    use Carbon\Carbon;
    
    class ProxyController extends Controller
    {
        const PROXY_CACHE = 5; # Cache duration in minutes
        const PROXYLINKVALIDHOURS = 1;
    
        public function proxyPage(Request $request)
        {
            if(!$request->filled("url") || !$request->filled("password")){
                if (env("APP_ENV", "") !== "production") {
                    return view("development");
                } else {
                    return redirect("https://metager.de");
                }
            }
    
            $targetUrl = $request->input("url", "https://metager.de");
            $password = $request->input("password", "");
    
            # Check For URL-Parameters that don't belong to the Proxy but to the URL that needs to be proxied
            $params = $request->except(['url', 'password']);
            if (sizeof($params) > 0) {
                # There are Params that need to be passed to the page
                # Most of the times this happens due to forms that are submitted on a proxied page
                # Let's redirect to the correct URI
                $proxyParams = $request->except(array_keys($params));
                $redirProxyUrl = $targetUrl;
                $redirParams = [];
                if (strpos($redirProxyUrl, "?") === false) {
                    $redirProxyUrl .= "?";
                } else {
                    # There are already Params for this site which need to get updated
                    $tmpParams = substr($redirProxyUrl, strpos($redirProxyUrl, "?") + 1);
                    $tmpParams = explode("&", $tmpParams);
                    foreach ($tmpParams as $param) {
                        $tmp = explode("=", $param);
                        if (sizeof($tmp) === 2) {
                            $redirParams[$tmp[0]] = $tmp[1];
                        }
                    }
                }
    
                foreach ($params as $key => $value) {
                    $redirParams[$key] = $value;
                }
    
                foreach ($redirParams as $key => $value) {
                    $redirProxyUrl .= $key . "=" . urlencode($value) . "&";
                }
    
                $redirProxyUrl = rtrim($redirProxyUrl, "&");
    
                $pw = md5(env('PROXY_PASSWORD') . $redirProxyUrl);
    
                $redirProxyUrl = base64_encode(str_rot13($redirProxyUrl));
                $redirProxyUrl = urlencode(str_replace("/", "<<SLASH>>", $redirProxyUrl));
    
                $proxyParams['url'] = $redirProxyUrl;
                $proxyParams['password'] = $pw;
    
                $newLink = action('ProxyController@proxyPage', $proxyParams);
                return redirect($newLink);
            }
    
            // Check Password
            if(!self::checkPassword($targetUrl, null, $password)){
                abort(400, "Invalid Request");
            }
    
            // Deny Loading internal URLs and check if URL syntax is correct
            $host = parse_url($targetUrl, PHP_URL_HOST);
            $selfHost = $request->getHttpHost();
            // The target URL couldn't be parsed. This is probably a malformed URL
            if($host === false){
                abort(404, "Invalid Request");
            }
            // The URL to load itself is a URL to our proxy
            // We will just redirect to that URL
            if ($host === $selfHost) {
                return redirect($targetUrl);
            }
    
            $this->writeLog($targetUrl, $request->ip());
           
            $urlToProxy = self::generateProxyUrl($targetUrl);
            
            return view('ProxyPage')
                ->with('iframeUrl', $urlToProxy)
                ->with('targetUrl', $targetUrl);
        }
    
        public function proxy(Request $request)
        {
            if(!$request->filled("url") || !$request->filled("password") || !$request->filled("valid-until")){
                Log::info("Request with missing url, password or valid-until");
                abort(400, "Invalid Request");
            }
    
            $targetUrl = $request->input("url", "https://metager.de");
            $password = $request->input("password", "");
            $validUntil = $request->input("valid-until", "");
    
            // Check Password
            if(!self::checkPassword($targetUrl, $validUntil, $password)){
                Log::info("Password incorrect");
                abort(400, "Invalid Request");
            }
    
            try {
                $validUntil = Carbon::createFromFormat("d-m-Y H:i:s P", $validUntil);
            } catch (InvalidFormatException $e) {
                abort(400, "Invalid Request");
            }
    
            if ($validUntil->isBefore(Carbon::now()->setTimezone("UTC"))) {
                Log::info("URL expired");
                abort(400, "Invalid Request");
            }
    
            // Deny Loading internal URLs and check if URL syntax is correct
            $host = parse_url($targetUrl, PHP_URL_HOST);
            $selfHost = $request->getHttpHost();
            // The target URL couldn't be parsed. This is probably a malformed URL
            // The URL to load itself is a URL to our proxy
            if($host === false || $host === $selfHost){
                Log::info("URL to myself");
                abort(404, "Invalid Request");
            }
    
            // Hash Value under which a possible cached file would've been stored
            $hash = md5($targetUrl);
            $httpcode = 200;
    
            if (!Cache::has($hash) || env("CACHE_ENABLED") === false) {
                $useragent = $_SERVER['HTTP_USER_AGENT'];
                if (preg_match('/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i', $useragent) || preg_match('/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i', substr($useragent, 0, 4))) {
                    // Mobile Browser Dummy Mobile Useragent
                    $useragent = 'Mozilla/5.0 (Android 10; Mobile; rv:83.0) Gecko/83.0 Firefox/83.0';
                } else {
                    // Not Mobile Dummy Desktop useragent
                    $useragent = 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:83.0) Gecko/20100101 Firefox/83.0';
                }
    
                $mission = [
                    "resulthash" => $hash,
                    "url" => $targetUrl,
                    "useragent" => $useragent,
                    "cacheDuration" => $this::PROXY_CACHE,
                ];
    
                $mission = json_encode($mission);
                Redis::rpush(RequestFetcher::FETCHQUEUE_KEY, $mission);
                $answer = Redis::brpoplpush($hash, $hash, 10);
                Redis::expire($hash, 15);
                if ($answer) {
                    $answer = json_decode($answer, true);
                }
            } else {
                $answer = Cache::get($hash);
            }
    
            if (!empty($answer["error"])) {
                if ($answer["error"] === CURLE_ABORTED_BY_CALLBACK) {
                    // File Downloads aren't working anymore within an IFrame.
                    // We will show the user a page to download the File
                    $postData = \App\Http\Controllers\DownloadController::generateDownloadLinkParameters($targetUrl);
                    return response(view("errors.413")->with([
                        "url" => $postData["url"],
                        "validuntil" => $postData["valid-until"],
                        "password" => $postData["password"]
                    ]), 413);
                }
            }
    
            if ($answer === null) {
                abort(400, "Couldn't fetch response", [
                    "url" => $targetUrl
                ]);
            } else {
                $httpcode = $answer["http-code"];
                extract(parse_url($targetUrl));
                $base = $scheme . "://" . $host;
    
                $headerArray = [];
    
                foreach ($answer["headers"] as $index => $value) {
                    if (strtolower($index) === "location") {
                        $redLink = $value;
                        if (strpos($redLink, "/") === 0) {
                            $parse = parse_url($targetUrl);
                            $redLink = $parse["scheme"] . "://" . $parse["host"] . $redLink;
                        } elseif (preg_match("/^\w+\.\w+$/si", $redLink)) {
                            $parse = parse_url($targetUrl);
                            $redLink = $parse["scheme"] . "://" . $parse["host"] . "/" . $redLink;
                        }
                        
                        $key = md5($request->ip() . microtime(true));
                        $headerArray[trim($index)] = self::generateProxyUrl($redLink);
                    } elseif (strtolower($index) === "content-disposition") {
                        $headerArray[strtolower(trim($index))] = strtolower(trim($value));
                    } else {
                        $headerArray[trim($index)] = trim($value);
                    }
                }
                $answer["headers"] = $headerArray;
    
                # It might happen that a server doesn't give Information about file Type.
                # Let's try to generate one in this case
                if (!isset($answer["headers"]["content-type"])) {
                    $finfo = new finfo(FILEINFO_MIME);
                    $answer["headers"]["content-type"] = $finfo->buffer(base64_decode($answer["body"]));
                }
    
                # We will parse whether we have a parser for this document type.
                # If not, we will not Proxy it:
                $contentTypeHeader = $answer["headers"]["content-type"];
                $contentType = strpos($answer["headers"]["content-type"], ";") !== false ? trim(substr($answer["headers"]["content-type"], 0, strpos($answer["headers"]["content-type"], ";"))) : trim($answer["headers"]["content-type"]);
                $contentEncoding = stripos($contentTypeHeader, "charset=") !== false ? trim(substr($contentTypeHeader, stripos($contentTypeHeader, "charset=") + 8)) : null;
                $contentEncoding = rtrim($contentEncoding, ";");
                if (isset($answer["headers"]["content-disposition"]) && stripos(trim($answer["headers"]["content-type"]), "image/") !== 0) {
                    // File Downloads aren't working anymore within an IFrame.
                    // We will show the user a page to download the File
                    $postData = \App\Http\Controllers\DownloadController::generateDownloadLinkParameters($targetUrl);
                    return response(view("downloadrequired")->with([
                        "url" => $postData["url"],
                        "validuntil" => $postData["valid-until"],
                        "password" => $postData["password"]
                    ]), 413);
                }
                $body = base64_decode($answer["body"]);
                switch ($contentType) {
                    case 'text/html':
                        # It's an html document
                        $htmlDocument = new HtmlDocument($password, $targetUrl, $body, $contentEncoding);
                        $htmlDocument->proxifyContent();
                        $answer['headers']['content-type'] = $contentType . "; charset=" . $htmlDocument->getEncoding();
                        $body = $htmlDocument->getResult();
                        break;
                    case 'application/pdf':
                        // We will download all PDF Files
                        // File Downloads aren't working anymore within an IFrame.
                        // We will show the user a page to download the File
                        $postData = \App\Http\Controllers\DownloadController::generateDownloadLinkParameters($targetUrl);
                        return response(view("downloadrequired")->with([
                            "url" => $postData["url"],
                            "validuntil" => $postData["valid-until"],
                            "password" => $postData["password"]
                        ]), 413);
                        break;
                        // no break
                    case 'image/png':
                    case 'image/jpeg':
                    case 'image/gif':
                    case 'image/webp':
                    case 'image/vnd.microsoft.icon':
                    case 'application/font-woff':
                    case 'application/x-font-woff':
                    case 'application/x-empty':
                    case 'font/woff2':
                    case 'image/svg+xml':
                    case 'application/octet-stream':
                    case 'text/plain':
                    case 'image/x-icon':
                    case 'font/eot':
                    case 'application/vnd.ms-fontobject':
                    case 'application/x-font-ttf':
                    case 'application/x-www-form-urlencoded':
                    case 'application/zip':
                    case 'binary/octet-stream':
                    case 'application/vnd.android.package-archive':
                    case 'image/svg+xml':
                    case 'font/woff':
                    case 'font/woff2':
                        # Nothing to do with Images: Just return them
                        break;
                    case 'text/css':
                        # Css Documents might contain references to External Documents that need to get Proxified
                        $cssDocument = new CssDocument($password, $targetUrl, $body);
                        $cssDocument->proxifyContent();
                        $body = $cssDocument->getResult();
                        break;
                    default:
                        # We have no Parser for this one. Let's respond:
                        Log::error("Couldn't find parser for content type " . $contentType . " on URL " . $targetUrl);
                        abort(500, $contentType . " " . $targetUrl);
                        break;
                }
            }
    
            if ($body === false) {
                $body = "";
            }
            $answer["headers"]["mgproxy-targeturl"] = $targetUrl;
            return response($body, $httpcode)
                ->withHeaders($answer["headers"]);
        }
    
        /**
         * This function is called if a proxied page submits a form
         * It should take the submitted parameters and add them to the url
         * After that it should redirect to the correct page with the correct parameters
         */
        public function formget(Request $request, $password, $validUntil, $url){
            if(empty($password) || empty($validUntil) || empty($url)){
                abort(400, "Invalid Request");
            }
    
            // Check Password
            if(!self::checkPassword($url, $validUntil, $password)){
                abort(400, "Invalid Request");
            }
    
            try {
                $validUntil = Carbon::createFromFormat("d-m-Y H:i:s P", $validUntil);
            } catch (InvalidFormatException $e) {
                abort(400, "Invalid Request");
            }
    
            if ($validUntil->isBefore(Carbon::now()->setTimezone("UTC"))) {
                abort(400, "Invalid Request");
            }
    
            // Deny Loading internal URLs and check if URL syntax is correct
            $host = parse_url($url, PHP_URL_HOST);
            $selfHost = $request->getHttpHost();
            // The target URL couldn't be parsed. This is probably a malformed URL
            // The URL to load itself is a URL to our proxy
            if($host === false || $host === $selfHost){
                abort(404, "Invalid Request");
            }
    
            // All Checks passed we can generate a url where the submitted data is included
            $submittedParameters = $request->all();
    
            // The URL itself might contain query parameters
            $containedParameters = array();
            $parts = parse_url($url);
            if(!empty($parts["query"])){
                parse_str($parts["query"], $containedParameters);
            }
            $urlParameters = array_merge($submittedParameters, $containedParameters);
    
            if(empty($parts["scheme"]) || empty($parts["host"])){
                abort(400, "Invalid Request");
            }
    
            // Build the url
            $targetUrl = $parts["scheme"] . "://" .
                ((!empty($parts["user"]) && !empty($parts["pass"])) ? $parts["user"] . ":" . $parts["pass"] . "@" : "") .
                $parts["host"] .
                (!empty($parts["port"]) ? ":" . $parts["port"] : "") .
                (!empty($parts["path"]) ? $parts["path"] : "") .
                (!empty($urlParameters) ? "?" . http_build_query($urlParameters, "", "&", PHP_QUERY_RFC3986) : "") .
                (!empty($parts["fragment"]) ? "#" . $parts["fragment"] : "");
    
            return redirect(self::generateProxyWrapperUrl($targetUrl));
        }
    
        /**
         * This function generates a URL to a proxied page
         * including the proxy header.
         */
        public static function generateProxyWrapperUrl($url){
            $password = self::generatePassword($url, null);
            $sanitizedUrl = self::sanitizeUrl($url);
    
            $sanitizedParts = parse_url($sanitizedUrl);
            $host = null;
            $path = null;
    
            if(!empty($sanitizedParts["host"])){
                $host = $sanitizedParts["host"];
            }
            if(!empty($sanitizedParts["path"])){
                $path = trim($sanitizedParts["path"], "/");
            }
    
    
    
            $parameters = [
                "host" => $host,
                "path" => $path,
                "url" => $url,
                "password" => $password,
            ];
    
            return route('proxy-wrapper-page', $parameters);
        }
    
        /**
         * This function generates a URL to a proxied page
         * excluding the proxy header.
         */
        public static function generateProxyUrl($url){
    
            $validUntil = self::generateValidUntilDate();
            $password = self::generatePassword($url, $validUntil);
    
            $sanitizedUrl = self::sanitizeUrl($url);
            $sanitizedParts = parse_url($sanitizedUrl);
            $host = null;
            $path = null;
    
            if(!empty($sanitizedParts["host"])){
                $host = $sanitizedParts["host"];
            }
            if(!empty($sanitizedParts["path"])){
                $path = trim($sanitizedParts["path"], "/");
            }
    
            $parameters = [
                "host" => $host,
                "path" => $path,
                "url" => $url,
                "valid-until" => $validUntil,
                "password" => $password,
            ];
    
            try{
                return route('proxy', $parameters);
            }catch (\Exception $e){
                $test = "test";
            }
        }
    
        /**
         * This function generates a URL to a page that takes submitted form data
         * excluding the proxy header.
         */
        public static function generateFormgetUrl($url){
    
            $validUntil = self::generateValidUntilDate();
            $password = self::generatePassword($url, $validUntil);
    
            $parameters = [
                "url" => $url,
                "validUntil" => $validUntil,
                "password" => $password,
            ];
    
            return route('proxy-formget', $parameters);
        }
    
        /**
         * This function generates a Date/Time String which is used by our Download Controller
         * to check if a download link is valid.
         * This Date/Time is used in the password hash, too to make sure it is not altered
         */
        private static function generateValidUntilDate()
        {
            $validUntil = Carbon::now()->setTimezone("UTC");
            $validUntil->addHours(self::PROXYLINKVALIDHOURS);
    
            return $validUntil->format("d-m-Y H:i:s P");
        }
    
        /**
         * This function generates the password for Download Links
         * The password is an hmac with the proxy password.
         * Algo is SHA256
         * Data is $url . $validUntil or just $url when it should not expire
         */
        private static function generatePassword($url, $validUntil)
        {
            $data = rtrim($url, "/");
    
            if(!empty($validUntil)){
                $data .= $validUntil;
            }
    
            if (!is_string($data) || strlen($data) === 0) {
                return null;
            }
            return hash_hmac("sha256", $data, env("PROXY_PASSWORD", "unsecure_password"));
        }
    
        private static function checkPassword($url, $validUntil, $password)
        {
            $data = rtrim($url, "/");
    
            if(!empty($validUntil)){
                $data .= $validUntil;
            }
    
            if (!is_string($data) || strlen($data) === 0) {
                return false;
            }
            $excpectedHash = hash_hmac("sha256", $data, env("PROXY_PASSWORD", "unsecure_password"));
            return hash_equals($excpectedHash, $password);
        }
    
        private function writeLog($targetUrl, $ip)
        {
            $logFile = env('PROXY_LOG_LOCATION');
    
            $dateString = date('D M d H:i:s Y');
    
            $logString = $dateString . "\t" . $targetUrl . "\t" . $ip . "\n";
            if (file_exists($logFile)) {
                file_put_contents($logFile, $logString, FILE_APPEND);
            }
        }
    
        private static function sanitizeUrl($url){
            $parts = parse_url($url);
    
            // Optional but we only sanitize URLs with scheme and host defined
            if($parts === false || empty($parts["scheme"]) || empty($parts["host"])){
                return $url;
            }
    
            $sanitizedPath = null;
            if(!empty($parts["path"])){
                $pathParts = explode("/", $parts["path"]);
                foreach($pathParts as $index => $pathPart){
                    if($index === 0) continue;
                    // The Path part might already be urlencoded
                    $sanitizedPath .= "/" . rawurlencode(rawurldecode($pathPart));
                }
            }
    
            // Build the url
            $targetUrl = $parts["scheme"] . "://" .
                ((!empty($parts["user"]) && !empty($parts["pass"])) ? $parts["user"] . ":" . $parts["pass"] . "@" : "") .
                rtrim($parts["host"], ".") .
                (!empty($parts["port"]) ? ":" . $parts["port"] : "") .
                (!empty($sanitizedPath) ? $sanitizedPath : "") .
                (!empty($parts["query"]) ? "?" . $parts["query"] : "") .
                (!empty($parts["fragment"]) ? "#" . $parts["fragment"] : "");
    
            return $targetUrl;
        }
    }