From 2735db464ac3bdf15b7c1b8df1b53326097a62fe Mon Sep 17 00:00:00 2001
From: Dominik Hebeler <dominik@suma-ev.de>
Date: Tue, 15 Sep 2020 13:30:19 +0200
Subject: [PATCH] added browserverification

---
 app/Http/Controllers/HumanVerification.php    | 81 +++++++++++++++++++
 app/Http/Kernel.php                           |  1 +
 app/Http/Middleware/BrowserVerification.php   | 62 ++++++++++++++
 chart/templates/deployment.yaml               |  6 ++
 composer.json                                 | 11 +--
 config/metager/metager.php                    |  5 ++
 resources/lang/de/429.php                     |  6 ++
 resources/lang/en/429.php                     |  6 ++
 resources/views/errors/404.blade.php          |  6 ++
 resources/views/errors/429.blade.php          | 14 ++++
 .../resultpage/verificationHeader.blade.php   |  5 ++
 routes/web.php                                |  3 +-
 12 files changed, 200 insertions(+), 6 deletions(-)
 create mode 100644 app/Http/Middleware/BrowserVerification.php
 create mode 100644 config/metager/metager.php
 create mode 100644 resources/lang/de/429.php
 create mode 100644 resources/lang/en/429.php
 create mode 100644 resources/views/errors/429.blade.php
 create mode 100644 resources/views/layouts/resultpage/verificationHeader.blade.php

diff --git a/app/Http/Controllers/HumanVerification.php b/app/Http/Controllers/HumanVerification.php
index 0ff75e435..6f5462314 100644
--- a/app/Http/Controllers/HumanVerification.php
+++ b/app/Http/Controllers/HumanVerification.php
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
 
 use Captcha;
 use Carbon;
+use Cookie;
 use Illuminate\Hashing\BcryptHasher as Hasher;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Cache;
@@ -270,4 +271,84 @@ class HumanVerification extends Controller
         HumanVerification::saveUser($user);
         return redirect('admin/bot');
     }
+
+    public function browserVerification(Request $request)
+    {
+        $key = $request->input("id", "");
+
+        // Verify that key is a md5 checksum
+        if (!preg_match("/^[a-f0-9]{32}$/", $key)) {
+            abort(404);
+        }
+
+        Redis::connection("cache")->pipeline(function ($redis) use ($key) {
+            $redis->rpush($key, true);
+            $redis->expire($key, 30);
+        });
+
+        return response("", 200)->header("Content-Type", "text/css");
+    }
+
+    public static function block(Request $request)
+    {
+        $prefix = "humanverification";
+
+        $ip = $request->ip();
+        $id = "";
+        $uid = "";
+        if (\App\Http\Controllers\HumanVerification::couldBeSpammer($ip)) {
+            $id = hash("sha1", "999.999.999.999");
+            $uid = hash("sha1", "999.999.999.999" . $ip . $_SERVER["AGENT"] . "uid");
+        } else {
+            $id = hash("sha1", $ip);
+            $uid = hash("sha1", $ip . $_SERVER["AGENT"] . "uid");
+        }
+
+        /**
+         * If the user sends a Password or a key
+         * We will not verificate the user.
+         * If someone that uses a bot finds this out we
+         * might have to change it at some point.
+         */
+        if ($request->filled('password') || $request->filled('key') || Cookie::get('key') !== null || $request->filled('appversion') || !env('BOT_PROTECTION', false)) {
+            $update = false;
+            return $next($request);
+        }
+
+        # Get all Users of this IP
+        $users = Cache::get($prefix . "." . $id, []);
+
+        $user = [];
+        $changed = false;
+        if (empty($users[$uid])) {
+            $user = [
+                'uid' => $uid,
+                'id' => $id,
+                'unusedResultPages' => 0,
+                'whitelist' => false,
+                'locked' => true,
+                "lockedKey" => "",
+                "expiration" => now()->addWeeks(2),
+            ];
+            $changed = true;
+        } else {
+            $user = $users[$uid];
+            if (!$user["locked"]) {
+                $user["locked"] = true;
+                $changed = true;
+            }
+        }
+
+        if ($user["whitelist"]) {
+            $user["expiration"] = now()->addWeeks(2);
+        } else {
+            $user["expiration"] = now()->addHours(72);
+        }
+        if ($changed) {
+            $userList = Cache::get($prefix . "." . $user["id"], []);
+            $userList[$user["uid"]] = $user;
+            Cache::put($prefix . "." . $user["id"], $userList, 2 * 7 * 24 * 60 * 60);
+        }
+        return [$id, $uid];
+    }
 }
diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php
index 24a8c577a..cb14ae702 100644
--- a/app/Http/Kernel.php
+++ b/app/Http/Kernel.php
@@ -62,5 +62,6 @@ class Kernel extends HttpKernel
         'referer.check' => \App\Http\Middleware\RefererCheck::class,
         'humanverification' => \App\Http\Middleware\HumanVerification::class,
         'useragentmaster' => \App\Http\Middleware\UserAgentMaster::class,
+        'browserverification' => \App\Http\Middleware\BrowserVerification::class,
     ];
 }
diff --git a/app/Http/Middleware/BrowserVerification.php b/app/Http/Middleware/BrowserVerification.php
new file mode 100644
index 000000000..463fff13b
--- /dev/null
+++ b/app/Http/Middleware/BrowserVerification.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Closure;
+use GrahamCampbell\Throttle\Facades\Throttle;
+use Illuminate\Support\Facades\Redis;
+use \App\Http\Controllers\HumanVerification;
+
+class BrowserVerification
+{
+    /**
+     * Handle an incoming request.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \Closure  $next
+     * @return mixed
+     */
+    public function handle($request, Closure $next)
+    {
+        $bvEnabled = config("metager.metager.browserverification_enabled");
+        if (empty($bwEnabled) || !$bvEnabled) {
+            return $next($request);
+        }
+
+        // Check if throttled
+        $accept = Throttle::check($request, 8, 1);
+        if (!$accept) {
+            Throttle::hit($request, 8, 1);
+            abort(429);
+        }
+        header('Content-type: text/html; charset=utf-8');
+        header('X-Accel-Buffering: no');
+        ini_set('zlib.output_compression', 'Off');
+        ini_set('output_buffering', 'Off');
+        ini_set('output_handler', '');
+
+        ob_end_clean();
+
+        $key = md5($request->ip() . microtime(true));
+
+        echo (view('layouts.resultpage.verificationHeader')->with('key', $key)->render());
+        flush();
+
+        $answer = boolval(Redis::connection("cache")->blpop($key, 5));
+
+        if ($answer === true) {
+            return $next($request);
+        } else {
+            $accept = Throttle::attempt($request, 8, 1);
+            if (!$accept) {
+                abort(429);
+            }
+
+            # Lockout
+            $ids = HumanVerification::block($request);
+        }
+
+        return redirect()->route('captcha', ["id" => $ids[0], "uid" => $ids[1], "url" => url()->full()]);
+
+    }
+}
diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml
index 25bf29e5e..62ff4b8d2 100644
--- a/chart/templates/deployment.yaml
+++ b/chart/templates/deployment.yaml
@@ -63,6 +63,9 @@ spec:
       - name: blacklist-ad
         secret:
           secretName: metager-ad-blacklist
+      - name: metager-config
+        configMap:
+          name: metager
       containers:
       - name: {{ .Chart.Name }}-phpfpm
         image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
@@ -93,6 +96,9 @@ spec:
           initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
           timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }}
         volumeMounts:
+        - name: metager-config
+          mountPath: /html/config
+          readOnly: true
         - name: mglogs-persistent-storage
           mountPath: /html/storage/logs/metager
           readOnly: false
diff --git a/composer.json b/composer.json
index 1bc55bcf8..51824d44b 100644
--- a/composer.json
+++ b/composer.json
@@ -8,17 +8,18 @@
     ],
     "license": "MIT",
     "require": {
-        "laravel/framework": "5.8.*",
         "php": "^7.1.3",
+        "endclothing/prometheus_client_php": "^1.0",
         "fideloper/proxy": "^4.0",
-        "laravel/tinker": "^1.0",
         "globalcitizen/php-iban": "^2.6",
+        "graham-campbell/throttle": "^7.5",
         "jenssegers/agent": "^2.6",
+        "laravel/framework": "5.8.*",
+        "laravel/tinker": "^1.0",
         "mcamara/laravel-localization": "dev-master#13f418e481ed06f482e4fca87ec5ff67c2949373",
         "mews/captcha": "^2.2",
         "predis/predis": "^1.1",
-        "symfony/dom-crawler": "^4.1",
-        "endclothing/prometheus_client_php": "^1.0"
+        "symfony/dom-crawler": "^4.1"
     },
     "require-dev": {
         "beyondcode/laravel-dump-server": "^1.0",
@@ -69,4 +70,4 @@
             "@php artisan key:generate --ansi"
         ]
     }
-}
\ No newline at end of file
+}
diff --git a/config/metager/metager.php b/config/metager/metager.php
new file mode 100644
index 000000000..cb7825e6f
--- /dev/null
+++ b/config/metager/metager.php
@@ -0,0 +1,5 @@
+<?php
+
+return [
+    "browserverification_enabled" => true,
+];
diff --git a/resources/lang/de/429.php b/resources/lang/de/429.php
new file mode 100644
index 000000000..609117299
--- /dev/null
+++ b/resources/lang/de/429.php
@@ -0,0 +1,6 @@
+<?php
+
+return [
+    'title' => '429 - Zu viele Anfragen',
+    'text' => '',
+];
diff --git a/resources/lang/en/429.php b/resources/lang/en/429.php
new file mode 100644
index 000000000..ccc7579bf
--- /dev/null
+++ b/resources/lang/en/429.php
@@ -0,0 +1,6 @@
+<?php
+
+return [
+    'title' => '429 - Too many Requests',
+    'text' => '',
+];
diff --git a/resources/views/errors/404.blade.php b/resources/views/errors/404.blade.php
index f4f87a305..0f928a9db 100644
--- a/resources/views/errors/404.blade.php
+++ b/resources/views/errors/404.blade.php
@@ -3,6 +3,12 @@
 @section('title', 'Fehler 404 - Seite nicht gefunden')
 
 @section('content')
+	<style>
+		main#main-content {
+			align-items: center;
+			justify-content: center;
+		}
+	</style>
 	<h1>{{ trans('404.title') }}</h1>
 	<p>{{ trans('404.text') }}</p>
 @endsection
diff --git a/resources/views/errors/429.blade.php b/resources/views/errors/429.blade.php
new file mode 100644
index 000000000..4592b922a
--- /dev/null
+++ b/resources/views/errors/429.blade.php
@@ -0,0 +1,14 @@
+@extends('layouts.subPages')
+
+@section('title', trans('429.title'))
+
+@section('content')
+	<style>
+		main#main-content {
+			align-items: center;
+			justify-content: center;
+		}
+	</style>
+	<h1>{{ trans('429.title') }}</h1>
+	<p>{{ trans('429.text') }}</p>
+@endsection
diff --git a/resources/views/layouts/resultpage/verificationHeader.blade.php b/resources/views/layouts/resultpage/verificationHeader.blade.php
new file mode 100644
index 000000000..58e20f52f
--- /dev/null
+++ b/resources/views/layouts/resultpage/verificationHeader.blade.php
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <link rel="stylesheet" href="/index.css?id={{ $key }}">
diff --git a/routes/web.php b/routes/web.php
index ce435e617..ede8a3b07 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -195,13 +195,14 @@ Route::group(
             return redirect(LaravelLocalization::getLocalizedURL(LaravelLocalization::getCurrentLocale(), '/'));
         });
 
-        Route::match(['get', 'post'], 'meta/meta.ger3', 'MetaGerSearch@search')->middleware('humanverification', 'useragentmaster');
+        Route::match(['get', 'post'], 'meta/meta.ger3', 'MetaGerSearch@search')->middleware('browserverification', 'humanverification', 'useragentmaster');
 
         Route::get('meta/loadMore', 'MetaGerSearch@loadMore');
         Route::post('img/cat.jpg', 'HumanVerification@remove');
         Route::get('verify/metager/{id}/{uid}', ['as' => 'captcha', 'uses' => 'HumanVerification@captcha', 'middleware' => 'throttle:12,1']);
         Route::get('r/metager/{mm}/{pw}/{url}', ['as' => 'humanverification', 'uses' => 'HumanVerification@removeGet']);
         Route::post('img/dog.jpg', 'HumanVerification@whitelist');
+        Route::get('index.css', 'HumanVerification@browserVerification');
 
         Route::get('meta/picture', 'Pictureproxy@get');
         Route::get('clickstats', 'LogController@clicklog');
-- 
GitLab