diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c9faf419086453604244e7e46359fa09500130eb..0c8a4472935c11fcd2c900c79b38fa859bb71cb1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -35,18 +35,6 @@ stages: - performance - cleanup -prepare_secrets: - stage: prepare - image: alpine:latest - script: - - cp $ENV_FILE .env # Also provide env file - artifacts: - paths: - - .env - only: - - branches - - tags - prepare_node: stage: prepare image: node:10 @@ -92,7 +80,7 @@ build: review: variables: - HELM_UPGRADE_EXTRA_ARGS: --set service.externalPort=80 --set service.internalPort=80 --set service.commonName= --set ingress.tls.enabled=false --set ingress.annotations.kubernetes\.io/tls-acme="false" --set ingress.annotations.nginx\.ingress\.kubernetes\.io/ssl-redirect="false" + HELM_UPGRADE_VALUES_FILE: .gitlab/review-values.yaml ROLLOUT_RESOURCE_TYPE: deployment production: diff --git a/.gitlab/review-values.yaml b/.gitlab/review-values.yaml new file mode 100644 index 0000000000000000000000000000000000000000..93036cb7fa263d5e55957d157423d305cb4c5e1d --- /dev/null +++ b/.gitlab/review-values.yaml @@ -0,0 +1,17 @@ +service: + enabled: true + externalPort: 80 + internalPort: 80 + commonName: "" +ingress: + tls: + enabled: false + annotations: + kubernetes.io/tls-acme: "false" + nginx.ingress.kubernetes.io/ssl-redirect: "false" +readinessProbe: + path: "/healthz" + timeoutSeconds: 60 +livenessProbe: + path: "/healthz" + timeoutSeconds: 60 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index a8dcb97d0ae31a2306d26d9f484bd5c649379c3f..5c91c25150438878c712e72d9c645769acc9b303 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.11.3 +FROM alpine:3 RUN apk add --update \ nginx \ @@ -21,11 +21,13 @@ RUN apk add --update \ php7-dom \ php7-fileinfo \ php7-redis \ - && rm -rf /var/cache/apk/* + && rm -rf /var/cache/apk/* \ + && update-ca-certificates WORKDIR /html -RUN sed -i 's/;error_log = log\/php7\/error.log/error_log = \/dev\/stderr/g' /etc/php7/php-fpm.conf && \ +RUN wget -O /etc/ssl/cacert.pem "https://curl.haxx.se/ca/cacert.pem" && \ + sed -i 's/;error_log = log\/php7\/error.log/error_log = \/dev\/stderr/g' /etc/php7/php-fpm.conf && \ sed -i 's/;daemonize = yes/daemonize = no/g' /etc/php7/php-fpm.conf && \ sed -i 's/listen = 127.0.0.1:9000/listen = 9000/g' /etc/php7/php-fpm.d/www.conf && \ sed -i 's/;request_terminate_timeout = 0/request_terminate_timeout = 30/g' /etc/php7/php-fpm.d/www.conf && \ @@ -50,6 +52,7 @@ RUN sed -i 's/;error_log = log\/php7\/error.log/error_log = \/dev\/stderr/g' /et sed -i 's/;opcache.max_wasted_percentage=5/opcache.max_wasted_percentage=5/g' /etc/php7/php.ini && \ sed -i 's/;opcache.validate_timestamps=1/opcache.validate_timestamps=1/g' /etc/php7/php.ini && \ sed -i 's/;opcache.revalidate_freq=2/opcache.revalidate_freq=300/g' /etc/php7/php.ini && \ + sed -i 's/;curl.cainfo =/curl.cainfo ="\/etc\/ssl\/cacert.pem"/g' /etc/php7/php.ini && \ ln -s /dev/null /var/log/nginx/access.log && \ ln -s /dev/stdout /var/log/nginx/error.log && \ cp /usr/share/zoneinfo/Europe/Berlin /etc/localtime && \ diff --git a/DockerfileDev b/DockerfileDev index 555ddd722a3344dfa514ff33368e209a06f8322d..4fe5df9f8fae668fdc84d784ade60ae31ec2c5ad 100644 --- a/DockerfileDev +++ b/DockerfileDev @@ -26,7 +26,8 @@ RUN apk add --update \ WORKDIR /html -RUN sed -i 's/;error_log = log\/php7\/error.log/error_log = \/dev\/stderr/g' /etc/php7/php-fpm.conf && \ +RUN wget -O /etc/ssl/cacert.pem "https://curl.haxx.se/ca/cacert.pem" && \ + sed -i 's/;error_log = log\/php7\/error.log/error_log = \/dev\/stderr/g' /etc/php7/php-fpm.conf && \ sed -i 's/;daemonize = yes/daemonize = no/g' /etc/php7/php-fpm.conf && \ sed -i 's/listen = 127.0.0.1:9000/listen = 9000/g' /etc/php7/php-fpm.d/www.conf && \ sed -i 's/;request_terminate_timeout = 0/request_terminate_timeout = 900/g' /etc/php7/php-fpm.d/www.conf && \ @@ -43,6 +44,7 @@ RUN sed -i 's/;error_log = log\/php7\/error.log/error_log = \/dev\/stderr/g' /et sed -i 's/group = www-data/group = nginx/g' /etc/php7/php-fpm.d/www.conf && \ sed -i 's/;cgi.fix_pathinfo=1/cgi.fix_pathinfo=0/g' /etc/php7/php.ini && \ sed -i 's/expose_php = On/expose_php = Off/g' /etc/php7/php.ini && \ + sed -i 's/;curl.cainfo =/curl.cainfo ="\/etc\/ssl\/cacert.pem"/g' /etc/php7/php.ini && \ sed -i 's/;zend_extension=xdebug.so/zend_extension=xdebug.so/g' /etc/php7/conf.d/xdebug.ini && \ echo "xdebug.remote_enable = 1" >> /etc/php7/conf.d/xdebug.ini && \ echo "xdebug.remote_autostart = 1" >> /etc/php7/conf.d/xdebug.ini && \ diff --git a/app/Document.php b/app/Document.php index b3ceb31e9d3fb6e3cd821da099e100d955083187..39f2a277bc539ca94aba67693fd13df532eb02ba 100644 --- a/app/Document.php +++ b/app/Document.php @@ -2,7 +2,6 @@ namespace App; -use Illuminate\Http\Request; use URL; abstract class Document @@ -16,46 +15,43 @@ abstract class Document $this->baseUrl = $base; } - public function proxifyUrl($url, $topLevel) - { - // Only convert valid URLs + public function proxifyFormAction($url){ $url = trim($url); - if (stripos($url, "javascript:") === 0) { return ""; } - if (strpos($url, "http") !== 0 || strpos($url, URL::to('/')) === 0) { + // Deny Loading internal URLs and check if URL syntax is correct + $host = parse_url($url, PHP_URL_HOST); + $selfHost = \Request::getHttpHost(); + if (strpos($url, "http") !== 0 || $host === false || $host === $selfHost) { return $url; } - $urlToProxy = base64_encode(str_rot13($url)); - $urlToProxy = str_replace("/", "<<SLASH>>", $urlToProxy); - $urlToProxy = urlencode($urlToProxy); - - if ($topLevel) { - $params = \Request::all(); - - # Password - $pw = md5(env('PROXY_PASSWORD') . $url); - $urlToProxy = base64_encode(str_rot13($url)); - $urlToProxy = urlencode(str_replace("/", "<<SLASH>>", $urlToProxy)); + return \App\Http\Controllers\ProxyController::generateFormgetUrl($url); + } - # Params - $params['password'] = $pw; - $params['url'] = $urlToProxy; + public function proxifyUrl($url, $topLevel) + { + // Only convert valid URLs + $url = trim($url); - $iframeUrl = action('ProxyController@proxyPage', $params); - } else { - $params = \Request::all(); - $params['password'] = $this->password; - $params['url'] = $urlToProxy; - $params["id"] = \Request::route("id"); + if (stripos($url, "javascript:") === 0) { + return ""; + } - $iframeUrl = action('ProxyController@proxy', $params); + // Deny Loading internal URLs and check if URL syntax is correct + $host = parse_url($url, PHP_URL_HOST); + $selfHost = \Request::getHttpHost(); + if (strpos($url, "http") !== 0 || $host === false || $host === $selfHost) { + return $url; } - return $iframeUrl; + if($topLevel){ + return \App\Http\Controllers\ProxyController::generateProxyWrapperUrl($url); + }else{ + return \App\Http\Controllers\ProxyController::generateProxyUrl($url); + } } protected function convertRelativeToAbsoluteLink($rel) diff --git a/app/HtmlDocument.php b/app/HtmlDocument.php index d3db7e7e2b7dc44cfc8fc4c7e9b93f333443c479..b3dffb0336280ab3eabe12835e355bb5856ccc31 100644 --- a/app/HtmlDocument.php +++ b/app/HtmlDocument.php @@ -10,7 +10,7 @@ class HtmlDocument extends Document private $htmlString; private $encoding; - public function __construct($password, $baseUrl, $htmlString, $encoding) + public function __construct($password, $baseUrl, $htmlString, $encoding = null) { parent::__construct($password, $baseUrl); $this->htmlString = $this->convertEncoding($htmlString, $encoding); @@ -168,7 +168,7 @@ class HtmlDocument extends Document } # # And finally Proxify the Url - $action = $this->proxifyUrl($action, true); + $action = $this->proxifyFormAction($action); $form->setAttribute("action", $action); } @@ -179,6 +179,9 @@ class HtmlDocument extends Document # Convert all Links to the proxified Version # All of this Links should NOT target to the top Level $link->setAttribute("href", $this->proxifyUrl($link->getAttribute("href"), false)); + // We will proxify any File that is linked here which changes its content + // Integrity will therefor fail and as we do not know the correct content yet it needs to be removed + $link->removeAttribute("integrity"); } # All Iframes diff --git a/app/Http/Controllers/ProxyController.php b/app/Http/Controllers/ProxyController.php index fc28d7f19d9873ab50529525bf5e52f7617680df..2a409fdb74a5c4f85789e5669acac88d08527a92 100644 --- a/app/Http/Controllers/ProxyController.php +++ b/app/Http/Controllers/ProxyController.php @@ -6,32 +6,32 @@ use App\CssDocument; use App\HtmlDocument; use Cache; use finfo; +use Log; use Illuminate\Http\Request; -use Symfony\Component\HttpFoundation\StreamedResponse; -use URL; use Illuminate\Support\Facades\Redis; use App\Console\Commands\RequestFetcher; -use App\Models\HttpParser; use Carbon\Carbon; class ProxyController extends Controller { const PROXY_CACHE = 5; # Cache duration in minutes + const PROXYLINKVALIDHOURS = 1; - public function proxyPage(Request $request, $password, $url) + public function proxyPage(Request $request) { - $targetUrl = str_replace("<<SLASH>>", "/", $url); - $targetUrl = str_rot13(base64_decode($targetUrl)); - if (strpos($targetUrl, URL::to('/')) === 0) { - return redirect($targetUrl); + if(!$request->filled("url") || !$request->filled("password")){ + if (env("APP_ENV", "") !== "production") { + return view("development"); + } else { + return redirect("https://metager.de"); + } } - // Password already got checked by the middleware: - - $newPW = md5(env('PROXY_PASSWORD') . date('dmy')); + $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(['enableJS', 'enableCookies']); + $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 @@ -75,83 +75,73 @@ class ProxyController extends Controller return redirect($newLink); } - $this->writeLog($targetUrl, $request->ip()); - - $toggles = "111000A"; - - # Script Toggle Url: - $params = $request->all(); - $scriptsEnabled = false; - if ($request->has('enableJS')) { - $scriptsEnabled = true; - array_forget($params, 'enableJS'); - } else { - $toggles[1] = "0"; - $params['enableJS'] = "true"; + // Check Password + if(!self::checkPassword($targetUrl, null, $password)){ + abort(400, "Invalid Request"); } - $params['password'] = $password; - $params['url'] = $url; - $scriptUrl = action('ProxyController@proxyPage', $params); - //$scriptUrl = "javascript:alert('Diese Funktion wurde auf Grund von technischen Problemen vorerst deaktiviert. Sie können JavaScript wieder aktivieren sobald diese behoben wurden.');"; - - # Cookie Toggle Url: - $params = $request->all(); - $cookiesEnabled = false; - if ($request->has('enableCookies')) { - $cookiesEnabled = true; - array_forget($params, 'enableCookies'); - } else { - $toggles[0] = "0"; - $params['enableCookies'] = "true"; + // 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"); } - - $params['password'] = $password; - $params['url'] = $url; - $cookieUrl = action('ProxyController@proxyPage', $params); - - $settings = "u0"; - if ($cookiesEnabled && !$scriptsEnabled) { - $settings = "O0"; - } elseif (!$cookiesEnabled && $scriptsEnabled) { - $settings = "e0"; - } elseif ($cookiesEnabled && $scriptsEnabled) { - $settings = "80"; + // The URL to load itself is a URL to our proxy + // We will just redirect to that URL + if ($host === $selfHost) { + return redirect($targetUrl); } - $key = md5($request->ip() . microtime(true)); - $urlToProxy = $this->proxifyUrl($targetUrl, $newPW, $key, false); + $this->writeLog($targetUrl, $request->ip()); + + $urlToProxy = self::generateProxyUrl($targetUrl); return view('ProxyPage') - ->with('key', $key) ->with('iframeUrl', $urlToProxy) - ->with('scriptsEnabled', $scriptsEnabled) - ->with('scriptUrl', $scriptUrl) - ->with('cookiesEnabled', $cookiesEnabled) - ->with('cookieUrl', $cookieUrl) ->with('targetUrl', $targetUrl); } - public function proxy(Request $request, $password, $id, $url) + public function proxy(Request $request) { - $supportedContentTypes = [ - 'text/html', - ]; + 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"); + } - - $targetUrl = str_replace("<<SLASH>>", "/", $url); - $targetUrl = str_rot13(base64_decode($targetUrl)); - try { - $path = parse_url($targetUrl)["path"]; - } catch (\Exception $e) { - $path = ""; + $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"); } - $this->password = $password; // Hash Value under which a possible cached file would've been stored $hash = md5($targetUrl); - $result = []; $httpcode = 200; if (!Cache::has($hash) || env("CACHE_ENABLED") === false) { @@ -195,8 +185,8 @@ class ProxyController extends Controller } } - if ($result === null) { - return $this->streamFile($targetUrl); + if ($answer === null) { + abort(400, "Couldn't fetch response"); } else { $httpcode = $answer["http-code"]; extract(parse_url($targetUrl)); @@ -216,7 +206,7 @@ class ProxyController extends Controller } $key = md5($request->ip() . microtime(true)); - $headerArray[trim($index)] = $this->proxifyUrl($redLink, null, $key, false); + $headerArray[trim($index)] = self::generateProxyUrl($redLink); } elseif (strtolower($index) === "content-disposition") { $headerArray[strtolower(trim($index))] = strtolower(trim($value)); } else { @@ -286,6 +276,9 @@ class ProxyController extends Controller 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': @@ -296,6 +289,7 @@ class ProxyController extends Controller 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; } @@ -308,100 +302,190 @@ class ProxyController extends Controller ->withHeaders($answer["headers"]); } - public function streamFile(Request $request, $password, $id, $url) - { - /** - * Forms of proxied webpages might aswell Post to this URL - * Those need to be denied - */ - $check = md5(env('PROXY_PASSWORD') . date('dmy') . $request->ip()); - if (!$request->filled("force-download") && $request->input("check", "") === $check) { - abort(405); + /** + * 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"); } - $targetUrl = str_replace("<<SLASH>>", "/", $url); - $targetUrl = str_rot13(base64_decode($targetUrl)); - if (strpos($targetUrl, URL::to('/')) === 0) { - return redirect($targetUrl); + + // Check Password + if(!self::checkPassword($url, $validUntil, $password)){ + abort(400, "Invalid Request"); } - return $this->streamResponse($targetUrl); - } - private function streamResponse($url) - { - $headers = get_headers($url, 1); + 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"); + } - $filename = basename($url); + // All Checks passed we can generate a url where the submitted data is included + $submittedParameters = $request->all(); - # From the headers we need to remove the first Element since it's the status code: - $status = $headers[0]; - $status = intval(preg_split("/\s+/si", $status)[1]); - array_forget($headers, 0); + // 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); - # Add the Filename if it's not set: - if (!isset($headers["Content-Disposition"])) { - $headers["Content-Disposition"] = "inline; filename=\"" . $filename . "\""; - } elseif (preg_match("/filename=\"{0,1}(.*?)(\"|\s|$)/", $headers["Content-Disposition"], $matches)) { - $filename = $matches[1]; + if(empty($parts["scheme"]) || empty($parts["host"])){ + abort(400, "Invalid Request"); } - return response()->streamDownload(function () use ($url) { - # We are gonna stream a large file - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_HEADER, 0); - curl_setopt($ch, CURLOPT_BUFFERSIZE, 4096); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_LOW_SPEED_LIMIT, 50000); - curl_setopt($ch, CURLOPT_LOW_SPEED_TIME, 5); - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $data) { - echo($data); - }); - - curL_exec($ch); - - curl_close($ch); - }, $filename, $headers); + // 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)); } - public function proxifyUrl($url, $password = null, $key, $topLevel) - { - // Only convert valid URLs - $url = trim($url); - if (strpos($url, "http") !== 0 || strpos($url, URL::to('/')) === 0) { - return $url; + /** + * This function generates a URL to a proxied page + * including the proxy header. + */ + public static function generateProxyWrapperUrl($url){ + $password = self::generatePassword($url, null); + + $parts = parse_url($url); + $host = null; + $path = null; + + if(!empty($parts["host"])){ + $host = $parts["host"]; } + if(!empty($parts["path"])){ + $path = trim($parts["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); - if (!$password) { - $password = urlencode(\Request::route('password')); + $parts = parse_url($url); + $host = null; + $path = null; + + if(!empty($parts["host"])){ + $host = $parts["host"]; + } + if(!empty($parts["path"])){ + $path = trim($parts["path"], "/"); } - $urlToProxy = base64_encode(str_rot13($url)); - $urlToProxy = str_replace("/", "<<SLASH>>", $urlToProxy); - $urlToProxy = urlencode($urlToProxy); + $parameters = [ + "host" => $host, + "path" => $path, + "url" => $url, + "valid-until" => $validUntil, + "password" => $password, + ]; + + return route('proxy', $parameters); + } - if ($topLevel) { - $params = \Request::all(); + /** + * This function generates a URL to a page that takes submitted form data + * excluding the proxy header. + */ + public static function generateFormgetUrl($url){ - # Password - $pw = md5(env('PROXY_PASSWORD') . $url); - $urlToProxy = base64_encode(str_rot13($url)); - $urlToProxy = urlencode(str_replace("/", "<<SLASH>>", $urlToProxy)); + $validUntil = self::generateValidUntilDate(); + $password = self::generatePassword($url, $validUntil); - # Params - $params['password'] = $pw; - $params['url'] = $urlToProxy; - - $iframeUrl = action('ProxyController@proxyPage', $params); - } else { - $params = \Request::all(); - $params['password'] = $password; - $params['url'] = $urlToProxy; - $params["id"] = $key; - - $iframeUrl = action('ProxyController@proxy', $params); + $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 = $url; + + if(!empty($validUntil)){ + $data .= $validUntil; } - return $iframeUrl; + 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 = $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) diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 26548969f8ada798b62cd3c806d75cfc0370621b..da5c03cfed8a6b3a836de6c627d71295139f3537 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -61,7 +61,6 @@ class Kernel extends HttpKernel 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'checkpw' => \App\Http\Middleware\CheckPassword::class, - 'browserverification' => \App\Http\Middleware\Browserverification::class, ]; /** diff --git a/app/Http/Middleware/Browserverification.php b/app/Http/Middleware/Browserverification.php deleted file mode 100644 index 52a0aa4c607b51b4c7fd5ba3c3b1c61e3ae1c19b..0000000000000000000000000000000000000000 --- a/app/Http/Middleware/Browserverification.php +++ /dev/null @@ -1,34 +0,0 @@ -<?php - -namespace App\Http\Middleware; - -use Closure; -use Illuminate\Support\Facades\Redis; - -class Browserverification -{ - /** - * Handle an incoming request. - * - * @param \Illuminate\Http\Request $request - * @param \Closure $next - * @return mixed - */ - public function handle($request, Closure $next) - { - $redis = Redis::connection("central"); - - $key = $request->route('id'); - if (!preg_match("/^[a-f0-9]{32}$/", $key)) { - abort(404); - } - - $answer = $redis->brpoplpush($key, $key, 3); - - if ($answer === null) { - abort(404); - } - - return $next($request); - } -} diff --git a/app/Http/Middleware/CheckPassword.php b/app/Http/Middleware/CheckPassword.php index bfad434cd555f266e4da9e6e73b6fc32c22b7ee2..8ce977d74bd79cbaab53c83ca141f41ec623155d 100644 --- a/app/Http/Middleware/CheckPassword.php +++ b/app/Http/Middleware/CheckPassword.php @@ -32,7 +32,7 @@ class CheckPassword } // Check Password: - $checkPw = md5(env('PROXY_PASSWORD') . $targetUrl); + $checkPw = md5(env('PROXY_PASSWORD_OLD') . $targetUrl); $password = $request->route('password'); if ($checkPw === $password) { return $next($request); diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml index 66cef14d3fd3a6012059a82210e3c60d8c1eb1d7..228c92625a17b42edd54ff5c79592a590d087a70 100644 --- a/chart/templates/deployment.yaml +++ b/chart/templates/deployment.yaml @@ -44,6 +44,10 @@ spec: spec: imagePullSecrets: {{ toYaml .Values.image.secrets | indent 10 }} + volumes: + - name: env-files + secret: + secretName: {{ .Values.application.secretName }} containers: - name: {{ .Chart.Name }}-phpfpm image: {{ template "imagename" . }} @@ -60,6 +64,11 @@ spec: value: {{ .Values.gitlab.envName | quote }} - name: GITLAB_ENVIRONMENT_URL value: {{ .Values.gitlab.envURL | quote }} + volumeMounts: + - name: env-files + mountPath: /html/.env + subPath: ENV_FILE + readOnly: true ports: - name: "{{ .Values.service.name }}-phpfpm" containerPort: 9000 @@ -130,6 +139,11 @@ spec: value: {{ .Values.gitlab.envName | quote }} - name: GITLAB_ENVIRONMENT_URL value: {{ .Values.gitlab.envURL | quote }} + volumeMounts: + - name: env-files + mountPath: /html/.env + subPath: ENV_FILE + readOnly: true livenessProbe: exec: command: diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 8d7a9610c7daaf5c50856b363bb27602f9ecdc50..1264ba45ae3baaa67d87c7c48c6b55ced541aada 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -3,7 +3,6 @@ <meta charset="utf-8" /> <title>Proxy - MetaGer</title> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> -<link href="/index.css?id={{$key}}" rel="stylesheet" type="text/css" /> <link href="{{ mix('/css/all.css') }}" rel="stylesheet" type="text/css" /> </head> <body> diff --git a/routes/web.php b/routes/web.php index 02a33927647270b875ff3f2882da6329de6eb1f3..8109f5e30e800ce59ea6d4d41ee94dc1e6149644 100644 --- a/routes/web.php +++ b/routes/web.php @@ -23,28 +23,43 @@ Route::group(['prefix' => 'download'], function () { Route::get('/', 'DownloadController@download')->name("download"); }); -Route::get('/', function () { +// Route for form submissions +Route::get('formget/{password}/{validUntil}/{url}', 'ProxyController@formget') + ->name('proxy-formget') + ->where('password', '[A-Fa-f0-9]{64}') + ->where('url', '.*'); +// Route without Proxy Header +Route::get('p/{host?}/{path?}', 'ProxyController@proxy')->name('proxy')->where('host', '[^\.]+(\.[^\.]+)+')->where('path', '(.*)'); +// Route with Proxy Header +Route::get('{host?}/{path?}', 'ProxyController@proxyPage')->name('proxy-wrapper-page')->where('host', '[^\.]+(\.[^\.]+)+')->where('path', '(.*)'); + +Route::post('{host?}/{path?}', function (Request $request) { if (env("APP_ENV", "") !== "production") { - return view("development"); - } else { - return redirect("https://metager.de"); + $validatedData = $request->validate([ + 'url' => 'required|url|max:255', + ]); + $url = $request->input('url', 'https://metager.de'); + return redirect(\App\Http\Controllers\ProxyController::generateProxyWrapperUrl($url)); + }else{ + abort(400); } -}); +})->where('host', '[^\.]+(\.[^\.]+)+')->where('path', '(.*)'); -Route::get('index.css', 'BrowserVerification@verifyCss'); - -Route::post('/', function (Request $request) { - $validatedData = $request->validate([ - 'url' => 'required|url|max:255', - ]); - $url = $request->input('url', 'https://metager.de'); - $password = md5(env('PROXY_PASSWORD') . $url); - $url = base64_encode(str_rot13($url)); - $target = urlencode(str_replace("/", "<<SLASH>>", $url)); - return redirect(action('ProxyController@proxyPage', ['password' => $password, 'url' => $target])); -}); +/** + * This is our old Proxy route + * We will keep it temporarily to help users + * migrate to the new ones and redirect to the new one. + * 15.01.2021 + */ +Route::get('{password}/{url}', function(Request $request, $password, $url){ + $targetUrl = str_replace("<<SLASH>>", "/", $url); + $targetUrl = str_rot13(base64_decode($targetUrl)); + if (strpos($targetUrl, URL::to('/')) === 0) { + return redirect($targetUrl); + } -Route::get('{password}/{url}', 'ProxyController@proxyPage')->middleware('throttle:60:1')->middleware('checkpw'); + return redirect(\App\Http\Controllers\ProxyController::generateProxyWrapperUrl($targetUrl)); +})->middleware('throttle:60:1')->middleware('checkpw'); # Route that triggers streaming of Download Route::get('proxy/{password}/{id}/{url}', 'ProxyController@proxy')->middleware('checkpw:true'); Route::post('proxy/{password}/{id}/{url}', 'ProxyController@streamFile')->middleware('checkpw:true');