Skip to content
Snippets Groups Projects

Resolve "Increase readability of Proxy URLs"

Merged Dominik Hebeler requested to merge 22-increase-readability-of-proxy-urls into master
Files
4
@@ -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)
Loading