HumanVerification.php 13.9 KB
Newer Older
Dominik Hebeler's avatar
Dominik Hebeler committed
1
2
3
4
<?php

namespace App\Http\Controllers;

5
6
use Captcha;
use Carbon;
Dominik Hebeler's avatar
Dominik Hebeler committed
7
use Cookie;
8
use Illuminate\Hashing\BcryptHasher as Hasher;
Dominik Hebeler's avatar
Dominik Hebeler committed
9
use Illuminate\Http\Request;
10
use Illuminate\Support\Facades\Cache;
11
use Illuminate\Support\Facades\Redis;
Dominik Hebeler's avatar
Dominik Hebeler committed
12
13
14
15
use Input;

class HumanVerification extends Controller
{
16
    const PREFIX = "humanverification";
17
18
    const EXPIRELONG = 60 * 60 * 24 * 14;
    const EXPIRESHORT = 60 * 60 * 72;
19
    const TOKEN_PREFIX = "humanverificationtoken.";
20

21
    public static function captcha(Request $request, Hasher $hasher, $id, $uid, $url = null)
22
23
24
25
    {
        if ($url != null) {
            $url = base64_decode(str_replace("<<SLASH>>", "/", $url));
        } else {
Dominik Hebeler's avatar
Dominik Hebeler committed
26
27
28
29
30
31
32
33
34
35
36
            $url = $request->input('url', url("/"));
        }

        $protocol = "http://";

        if ($request->secure()) {
            $protocol = "https://";
        }

        if (stripos($url, $protocol . $request->getHttpHost()) !== 0) {
            $url = url("/");
37
        }
Dominik Hebeler's avatar
Dominik Hebeler committed
38

39
40
41
42
43
44
45
46
47
        $userlist = Cache::get(HumanVerification::PREFIX . "." . $id, []);
        $user = null;

        if (sizeof($userlist) === 0 || empty($userlist[$uid])) {
            return redirect('/');
        } else {
            $user = $userlist[$uid];
        }

48
        if ($request->getMethod() == 'POST') {
49
            \App\PrometheusExporter::CaptchaAnswered();
50
            $lockedKey = $request->input("c", "");
51

Dominik Hebeler's avatar
Dominik Hebeler committed
52
53
54
            $rules = ['captcha' => 'required|captcha_api:' . $lockedKey  . ',math'];
            $validator = validator()->make(request()->all(), $rules);

55
            if (empty($lockedKey) || $validator->fails()) {
56
                $captcha = Captcha::create("default", true);
57
                \App\PrometheusExporter::CaptchaShown();
58
                return view('humanverification.captcha')->with('title', 'Bestätigung notwendig')
59
                    ->with('uid', $user["uid"])
60
61
                    ->with('id', $id)
                    ->with('url', $url)
62
                    ->with('correct', $captcha["key"])
63
                    ->with('image', $captcha["img"])
Dominik Hebeler's avatar
Dominik Hebeler committed
64
                    ->with('errorMessage', 'Fehler: Falsche Eingabe!');
65
            } else {
66
                \App\PrometheusExporter::CaptchaCorrect();
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
                # Generate a token that makes the user skip Humanverification
                # There are some special cases where a user that entered a correct Captcha
                # might see a captcha again on his next request
                $token = md5(microtime(true));
                Cache::put(self::TOKEN_PREFIX . $token, 5, 3600);
                $url_parts = parse_url($url);
                // If URL doesn't have a query string.
                if (isset($url_parts['query'])) { // Avoid 'Undefined index: query'
                    parse_str($url_parts['query'], $params);
                } else {
                    $params = array();
                }

                $params['token'] = $token;     // Overwrite if exists

                // Note that this will url_encode all values
                $url_parts['query'] = http_build_query($params);

                // If not
                $url = $url_parts['scheme'] . '://' . $url_parts['host'] . (!empty($url_parts["port"]) ? ":" . $url_parts["port"] : "") . $url_parts['path'] . '?' . $url_parts['query'];

88
                # If we can unlock the Account of this user we will redirect him to the result page
89
                if ($user !== null && $user["locked"]) {
Dominik Hebeler's avatar
Dominik Hebeler committed
90
                    # The Captcha was correct. We can remove the key from the user
91
92
93
94
                    # Additionally we will whitelist him so he is not counted towards botnetwork
                    $user["locked"] = false;
                    $user["whitelist"] = true;
                    HumanVerification::saveUser($user);
Dominik Hebeler's avatar
Dominik Hebeler committed
95
                    return redirect($url);
96
                } else {
Dominik Hebeler's avatar
Dominik Hebeler committed
97
98
99
100
                    return redirect('/');
                }
            }
        }
Dominik Hebeler's avatar
Dominik Hebeler committed
101

102
        $captcha = Captcha::create("default", true);
103
        \App\PrometheusExporter::CaptchaShown();
104
        return view('humanverification.captcha')->with('title', 'Bestätigung notwendig')
105
            ->with('uid', $user["uid"])
106
107
            ->with('id', $id)
            ->with('url', $url)
108
            ->with('correct', $captcha["key"])
109
            ->with('image', $captcha["img"]);
Dominik Hebeler's avatar
Dominik Hebeler committed
110
111
    }

112
113
    public static function logCaptcha(Request $request)
    {
114
        $fail2banEnabled = config("metager.metager.fail2ban.enabled");
115
        if (empty($fail2banEnabled) || !$fail2banEnabled || !config("metager.metager.fail2ban.url") || !config("metager.metager.fail2ban.user") || !config("metager.metager.fail2ban.password")) {
116
117
118
119
120
            return;
        }

        // Submit fetch job to worker
        $mission = [
121
122
123
124
125
126
127
128
129
130
131
            "resulthash" => "captcha",
            "url" => config("metager.metager.fail2ban.url") . "/captcha/",
            "useragent" => "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:81.0) Gecko/20100101 Firefox/81.0",
            "username" => config("metager.metager.fail2ban.user"),
            "password" => config("metager.metager.fail2ban.password"),
            "headers" => [
                "ip" => $request->ip()
            ],
            "cacheDuration" => 0,
            "name" => "Captcha",
        ];
132
133
134
135
        $mission = json_encode($mission);
        Redis::rpush(\App\MetaGer::FETCHQUEUE_KEY, $mission);
    }

136
137
138
    public static function remove(Request $request)
    {
        if (!$request->has('mm')) {
Dominik Hebeler's avatar
Dominik Hebeler committed
139
140
            abort(404, "Keine Katze gefunden.");
        }
141

142
        if (HumanVerification::checkId($request, $request->input('mm'))) {
143
            HumanVerification::removeUser($request, $request->input('mm'));
Dominik Hebeler's avatar
Dominik Hebeler committed
144
145
146
147
148
        }
        return response(hex2bin('89504e470d0a1a0a0000000d494844520000000100000001010300000025db56ca00000003504c5445000000a77a3dda0000000174524e530040e6d8660000000a4944415408d76360000000020001e221bc330000000049454e44ae426082'), 200)
            ->header('Content-Type', 'image/png');
    }

149
150
151
    public static function removeGet(Request $request, $mm, $password, $url)
    {
        $url = base64_decode(str_replace("<<SLASH>>", "/", $url));
Dominik Hebeler's avatar
Dominik Hebeler committed
152
        # If the user is correct and the password is we will delete any entry in the database
153
        $requiredPass = md5($mm . Carbon::NOW()->day . $url . config("metager.metager.proxy.password"));
Dominik Hebeler's avatar
Dominik Hebeler committed
154

155
        if (HumanVerification::checkId($request, $mm) && $requiredPass === $password) {
156
            HumanVerification::removeUser($request, $mm);
Dominik Hebeler's avatar
Dominik Hebeler committed
157
158
159
160
        }
        return redirect($url);
    }

161
162
163
    private static function saveUser($user)
    {
        $userList = Cache::get(HumanVerification::PREFIX . "." . $user["id"], []);
Dominik Hebeler's avatar
Dominik Hebeler committed
164

165
166
167
168
169
        if ($user["whitelist"]) {
            $user["expiration"] = now()->addWeeks(2);
        } else {
            $user["expiration"] = now()->addHours(72);
        }
Dominik Hebeler's avatar
Dominik Hebeler committed
170
        $userList[$user["uid"]] = $user;
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
        Cache::put(HumanVerification::PREFIX . "." . $user["id"], $userList, now()->addWeeks(2));
    }

    private static function deleteUser($user)
    {
        $userList = Cache::get(HumanVerification::PREFIX . "." . $user["id"], []);
        $newUserList = [];
        $changed = false;

        foreach ($userList as $uid => $userTmp) {
            if ($userTmp["uid"] !== $user["uid"]) {
                $newUserList[$userTmp["uid"]] = $userTmp;
            } else {
                $changed = true;
            }
        }
Dominik Hebeler's avatar
Dominik Hebeler committed
187

188
        if ($changed) {
Dominik Hebeler's avatar
Dominik Hebeler committed
189
190
191
192
193
            if (sizeof($newUserList) > 0) {
                Cache::put(HumanVerification::PREFIX . "." . $user["id"], $newUserList, now()->addWeeks(2));
            } else {
                Cache::forget(HumanVerification::PREFIX . "." . $user["id"], $newUserList);
            }
194
195
196
        }
    }

197
198
    private static function removeUser($request, $uid)
    {
Dominik Hebeler's avatar
Dominik Hebeler committed
199
200
        $ip = $request->ip();
        $id = "";
Dominik Hebeler's avatar
Bugfix    
Dominik Hebeler committed
201
        if (HumanVerification::couldBeSpammer($ip)) {
Dominik Hebeler's avatar
Dominik Hebeler committed
202
            $id = hash("sha1", "999.999.999.999");
Dominik Hebeler's avatar
Dominik Hebeler committed
203
        } else {
Dominik Hebeler's avatar
Dominik Hebeler committed
204
            $id = hash("sha1", $ip);
Dominik Hebeler's avatar
Dominik Hebeler committed
205
        }
206

207
208
209
210
211
212
213
        $userlist = Cache::get(HumanVerification::PREFIX . "." . $id, []);
        $user = null;

        if (sizeof($userlist) === 0 || empty($userlist[$uid])) {
            return;
        } else {
            $user = $userlist[$uid];
214
215
216
        }

        $sum = 0;
217
        foreach ($userlist as $uidTmp => $userTmp) {
Dominik Hebeler's avatar
Dominik Hebeler committed
218
            if (!empty($userTmp) && gettype($userTmp["whitelist"]) === "boolean" && !$userTmp["whitelist"]) {
219
220
                $sum += intval($userTmp["unusedResultPages"]);
            }
221
222
        }
        # Check if we have to whitelist the user or if we can simply delete the data
223
        if ($user["unusedResultPages"] < $sum && !$user["whitelist"]) {
224
            # Whitelist
225
            $user["whitelist"] = true;
226
227
        }

228
        if ($user["whitelist"]) {
229
230
            $user["unusedResultPages"] = 0;
            HumanVerification::saveUser($user);
231
        } else {
232
            HumanVerification::deleteUser($user);
233
234
235
        }
    }

236
237
    private static function checkId($request, $id)
    {
Dominik Hebeler's avatar
Dominik Hebeler committed
238
239
        $uid = "";
        $ip = $request->ip();
Dominik Hebeler's avatar
Bugfix    
Dominik Hebeler committed
240
        if (HumanVerification::couldBeSpammer($ip)) {
Dominik Hebeler's avatar
Dominik Hebeler committed
241
            $uid = hash("sha1", "999.999.999.999" . $ip . $_SERVER["AGENT"] . "uid");
Dominik Hebeler's avatar
Dominik Hebeler committed
242
        } else {
Dominik Hebeler's avatar
Dominik Hebeler committed
243
            $uid = hash("sha1", $ip . $_SERVER["AGENT"] . "uid");
Dominik Hebeler's avatar
Dominik Hebeler committed
244
245
246
        }

        if ($uid === $id) {
Dominik Hebeler's avatar
Dominik Hebeler committed
247
            return true;
248
        } else {
Dominik Hebeler's avatar
Dominik Hebeler committed
249
250
251
            return false;
        }
    }
Dominik Hebeler's avatar
Dominik Hebeler committed
252

Dominik Hebeler's avatar
Bugfix    
Dominik Hebeler committed
253
    public static function couldBeSpammer($ip)
Dominik Hebeler's avatar
Dominik Hebeler committed
254
    {
Dominik Hebeler's avatar
Bugfix    
Dominik Hebeler committed
255
256
        # Check for recent Spams
        $eingabe = \Request::input('eingabe');
257
        $spams = Redis::lrange("spam", 0, -1);
258
        foreach ($spams as $index => $spam) {
259
            if (\preg_match($spam, $eingabe)) {
260
                return "999.999.999.999" . $index;
261
            }
Dominik Hebeler's avatar
Bugfix    
Dominik Hebeler committed
262
263
        }

264
        return null;
Dominik Hebeler's avatar
Dominik Hebeler committed
265
    }
266

Dominik Hebeler's avatar
Dominik Hebeler committed
267
268
    public function botOverview(Request $request)
    {
269
270
271
272
        $id = "";
        $uid = "";
        $ip = $request->ip();
        if (\App\Http\Controllers\HumanVerification::couldBeSpammer($ip)) {
Dominik Hebeler's avatar
Dominik Hebeler committed
273
274
            $id = hash("sha1", "999.999.999.999");
            $uid = hash("sha1", "999.999.999.999" . $ip . $_SERVER["AGENT"] . "uid");
275
        } else {
Dominik Hebeler's avatar
Dominik Hebeler committed
276
277
            $id = hash("sha1", $ip);
            $uid = hash("sha1", $ip . $_SERVER["AGENT"] . "uid");
278
279
280
281
282
283
284
285
        }

        $userList = Cache::get(HumanVerification::PREFIX . "." . $id);
        $user = $userList[$uid];

        return view('humanverification.botOverview')
            ->with('title', "Bot Overview")
            ->with('ip', $ip)
286
            ->with('userList', $userList)
287
288
289
            ->with('user', $user);
    }

Dominik Hebeler's avatar
Dominik Hebeler committed
290
291
    public function botOverviewChange(Request $request)
    {
292
293
294
295
        $id = "";
        $uid = "";
        $ip = $request->ip();
        if (\App\Http\Controllers\HumanVerification::couldBeSpammer($ip)) {
Dominik Hebeler's avatar
Dominik Hebeler committed
296
297
            $id = hash("sha1", "999.999.999.999");
            $uid = hash("sha1", "999.999.999.999" . $ip . $_SERVER["AGENT"] . "uid");
298
        } else {
Dominik Hebeler's avatar
Dominik Hebeler committed
299
300
            $id = hash("sha1", $ip);
            $uid = hash("sha1", $ip . $_SERVER["AGENT"] . "uid");
301
302
303
304
305
        }

        $userList = Cache::get(HumanVerification::PREFIX . "." . $id);
        $user = $userList[$uid];

Dominik Hebeler's avatar
Dominik Hebeler committed
306
        if ($request->filled("locked")) {
307
            $user["locked"] = boolval($request->input('locked'));
Dominik Hebeler's avatar
Dominik Hebeler committed
308
        } elseif ($request->filled("whitelist")) {
309
            $user["whitelist"] = boolval($request->input('whitelist'));
Dominik Hebeler's avatar
Dominik Hebeler committed
310
        } elseif ($request->filled("unusedResultPages")) {
311
312
313
314
315
316
            $user["unusedResultPages"] = intval($request->input('unusedResultPages'));
        }

        HumanVerification::saveUser($user);
        return redirect('admin/bot');
    }
Dominik Hebeler's avatar
Dominik Hebeler committed
317

318
    public function browserVerification(Request $request)
Dominik Hebeler's avatar
Dominik Hebeler committed
319
320
321
322
323
324
325
326
    {
        $key = $request->input("id", "");

        // Verify that key is a md5 checksum
        if (!preg_match("/^[a-f0-9]{32}$/", $key)) {
            abort(404);
        }

327
328
        Redis::connection(config('cache.stores.redis.connection'))->rpush($key, true);
        Redis::connection(config('cache.stores.redis.connection'))->expire($key, 30);
Dominik Hebeler's avatar
Dominik Hebeler committed
329

330
        return response(view('layouts.resultpage.verificationCss'), 200)->header("Content-Type", "text/css");
Dominik Hebeler's avatar
Dominik Hebeler committed
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
    }

    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.
         */
354
        if ($request->filled('password') || $request->filled('key') || Cookie::get('key') !== null || $request->filled('appversion') || !config('metager.metager.botprotection.enabled')) {
Dominik Hebeler's avatar
Dominik Hebeler committed
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
            $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];
    }
Dominik Hebeler's avatar
Dominik Hebeler committed
395
}