diff options
author | Frankie B <git@diskfloppy.me> | 2023-07-16 01:49:09 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-07-16 01:49:09 +0100 |
commit | 8482a98ca6a767062917747a0b016a9ab4d35f01 (patch) | |
tree | 0ab0e8d3baf5b421ab909bc731334e2deff82fe0 | |
parent | 1b267f61029e7d4b824979709434ffb437c347b8 (diff) |
feat: add guestbook with rate limiting (#6)
* Re-add guestbook w/ rate limiting
* Add guestbook to navbar
-rw-r--r-- | app/Http/Controllers/GuestbookController.php | 29 | ||||
-rw-r--r-- | app/Http/Kernel.php | 5 | ||||
-rw-r--r-- | app/Http/Middleware/RateLimiter.php | 32 | ||||
-rw-r--r-- | composer.json | 3 | ||||
-rw-r--r-- | composer.lock | 138 | ||||
-rw-r--r-- | config/database.php | 81 | ||||
-rw-r--r-- | public/css/master.css | 40 | ||||
-rw-r--r-- | public/css/minimal.css | 11 | ||||
-rw-r--r-- | resources/views/errors/ratelimit-guestbook.blade.php | 16 | ||||
-rw-r--r-- | resources/views/includes/header.blade.php | 3 | ||||
-rw-r--r-- | resources/views/pages/guestbook.blade.php | 53 | ||||
-rw-r--r-- | routes/web.php | 9 |
12 files changed, 329 insertions, 91 deletions
diff --git a/app/Http/Controllers/GuestbookController.php b/app/Http/Controllers/GuestbookController.php new file mode 100644 index 0000000..aff30ed --- /dev/null +++ b/app/Http/Controllers/GuestbookController.php @@ -0,0 +1,29 @@ +<?php + +namespace App\Http\Controllers; + +use Illuminate\Http\Request; +use DB; + +class GuestbookController extends Controller { + public function guestbook() { + return view('pages.guestbook'); + } + + public function guestbookPost(Request $request) { + $this->validate($request, [ + 'name' => 'required', + 'message' => 'required' + ]); + + DB::insert('INSERT INTO guestbook_entries (name, timestamp, ip_address, agent, message) values (?, ?, ?, ?, ?)', array( + htmlspecialchars($request->get('name')), + time(), + $request->ip(), + $request->userAgent(), + htmlspecialchars($request->get('message')) + )); + + return back()->with('success', 'Entry submitted successfully!'); + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 1fb53dc..4eab7b8 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -45,6 +45,11 @@ class Kernel extends HttpKernel ], ]; + protected $routeMiddleware = [ + 'rate_limit' => \App\Http\Middleware\RateLimiter::class, + ]; + + /** * The application's middleware aliases. * diff --git a/app/Http/Middleware/RateLimiter.php b/app/Http/Middleware/RateLimiter.php new file mode 100644 index 0000000..c81da43 --- /dev/null +++ b/app/Http/Middleware/RateLimiter.php @@ -0,0 +1,32 @@ +<?php + +namespace App\Http\Middleware; + +use Closure; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Cache; +use Symfony\Component\HttpFoundation\Response; + +class RateLimiter +{ + /** + * Handle an incoming request. + * + * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next + */ + public function handle(Request $request, Closure $next): Response + { + $ipAddress = $request->ip(); + $cacheKey = 'rate_limit_' . $ipAddress; + + if (Cache::has($cacheKey)) { + // If the cache key exists, the IP has submitted an entry within the last hour + return response()->view('errors.ratelimit-guestbook', [], 429); + } + + // Add the IP address to the cache and set the expiration time to one hour + Cache::put($cacheKey, true, 60); + + return $next($request); + } +} diff --git a/composer.json b/composer.json index e4c5e7d..451f135 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,8 @@ "guzzlehttp/guzzle": "^7.2", "laravel/framework": "^10.10", "laravel/sanctum": "^3.2", - "laravel/tinker": "^2.8" + "laravel/tinker": "^2.8", + "spatie/laravel-honeypot": "^4.3" }, "require-dev": { "fakerphp/faker": "^1.9.1", diff --git a/composer.lock b/composer.lock index 4664da4..3018272 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "aa322c53454393ed775cfe4807d54a50", + "content-hash": "505f8d503188864625fc855900ea2202", "packages": [ { "name": "brick/math", @@ -2995,6 +2995,142 @@ "time": "2023-04-15T23:01:58+00:00" }, { + "name": "spatie/laravel-honeypot", + "version": "4.3.2", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-honeypot.git", + "reference": "eab92dd2096f1cdb83c28ced4f4632d3cfde2872" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-honeypot/zipball/eab92dd2096f1cdb83c28ced4f4632d3cfde2872", + "reference": "eab92dd2096f1cdb83c28ced4f4632d3cfde2872", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^8.0|^9.0|^10.0", + "illuminate/encryption": "^8.0|^9.0|^10.0", + "illuminate/http": "^8.0|^9.0|^10.0", + "illuminate/support": "^8.0|^9.0|^10.0", + "illuminate/validation": "^8.0|^9.0|^10.0", + "nesbot/carbon": "^2.0", + "php": "^8.0", + "spatie/laravel-package-tools": "^1.9", + "symfony/http-foundation": "^5.1.2|^6.0" + }, + "require-dev": { + "livewire/livewire": "^2.10", + "orchestra/testbench": "^6.23|^7.0|^8.0", + "pestphp/pest-plugin-livewire": "^1.0", + "phpunit/phpunit": "^9.4", + "spatie/pest-plugin-snapshots": "^1.1", + "spatie/phpunit-snapshot-assertions": "^4.2", + "spatie/test-time": "^1.2.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\Honeypot\\HoneypotServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Spatie\\Honeypot\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Preventing spam submitted through forms", + "homepage": "https://github.com/spatie/laravel-honeypot", + "keywords": [ + "laravel-honeypot", + "spatie" + ], + "support": { + "source": "https://github.com/spatie/laravel-honeypot/tree/4.3.2" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + } + ], + "time": "2023-01-17T07:09:34+00:00" + }, + { + "name": "spatie/laravel-package-tools", + "version": "1.15.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-package-tools.git", + "reference": "efab1844b8826443135201c4443690f032c3d533" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/efab1844b8826443135201c4443690f032c3d533", + "reference": "efab1844b8826443135201c4443690f032c3d533", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^9.28|^10.0", + "php": "^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.5", + "orchestra/testbench": "^7.7|^8.0", + "pestphp/pest": "^1.22", + "phpunit/phpunit": "^9.5.24", + "spatie/pest-plugin-test-time": "^1.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\LaravelPackageTools\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" + } + ], + "description": "Tools for creating Laravel packages", + "homepage": "https://github.com/spatie/laravel-package-tools", + "keywords": [ + "laravel-package-tools", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-package-tools/issues", + "source": "https://github.com/spatie/laravel-package-tools/tree/1.15.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2023-04-27T08:09:01+00:00" + }, + { "name": "symfony/console", "version": "v6.3.0", "source": { diff --git a/config/database.php b/config/database.php index 137ad18..67ae798 100644 --- a/config/database.php +++ b/config/database.php @@ -34,15 +34,6 @@ return [ */ 'connections' => [ - - 'sqlite' => [ - 'driver' => 'sqlite', - 'url' => env('DATABASE_URL'), - 'database' => env('DB_DATABASE', database_path('database.sqlite')), - 'prefix' => '', - 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), - ], - 'mysql' => [ 'driver' => 'mysql', 'url' => env('DATABASE_URL'), @@ -62,37 +53,6 @@ return [ PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), ]) : [], ], - - 'pgsql' => [ - 'driver' => 'pgsql', - 'url' => env('DATABASE_URL'), - 'host' => env('DB_HOST', '127.0.0.1'), - 'port' => env('DB_PORT', '5432'), - 'database' => env('DB_DATABASE', 'forge'), - 'username' => env('DB_USERNAME', 'forge'), - 'password' => env('DB_PASSWORD', ''), - 'charset' => 'utf8', - 'prefix' => '', - 'prefix_indexes' => true, - 'search_path' => 'public', - 'sslmode' => 'prefer', - ], - - 'sqlsrv' => [ - 'driver' => 'sqlsrv', - 'url' => env('DATABASE_URL'), - 'host' => env('DB_HOST', 'localhost'), - 'port' => env('DB_PORT', '1433'), - 'database' => env('DB_DATABASE', 'forge'), - 'username' => env('DB_USERNAME', 'forge'), - 'password' => env('DB_PASSWORD', ''), - 'charset' => 'utf8', - 'prefix' => '', - 'prefix_indexes' => true, - // 'encrypt' => env('DB_ENCRYPT', 'yes'), - // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), - ], - ], /* @@ -107,45 +67,4 @@ return [ */ 'migrations' => 'migrations', - - /* - |-------------------------------------------------------------------------- - | Redis Databases - |-------------------------------------------------------------------------- - | - | Redis is an open source, fast, and advanced key-value store that also - | provides a richer body of commands than a typical key-value system - | such as APC or Memcached. Laravel makes it easy to dig right in. - | - */ - - 'redis' => [ - - 'client' => env('REDIS_CLIENT', 'phpredis'), - - 'options' => [ - 'cluster' => env('REDIS_CLUSTER', 'redis'), - 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), - ], - - 'default' => [ - 'url' => env('REDIS_URL'), - 'host' => env('REDIS_HOST', '127.0.0.1'), - 'username' => env('REDIS_USERNAME'), - 'password' => env('REDIS_PASSWORD'), - 'port' => env('REDIS_PORT', '6379'), - 'database' => env('REDIS_DB', '0'), - ], - - 'cache' => [ - 'url' => env('REDIS_URL'), - 'host' => env('REDIS_HOST', '127.0.0.1'), - 'username' => env('REDIS_USERNAME'), - 'password' => env('REDIS_PASSWORD'), - 'port' => env('REDIS_PORT', '6379'), - 'database' => env('REDIS_CACHE_DB', '1'), - ], - - ], - ]; diff --git a/public/css/master.css b/public/css/master.css index 323c2ec..7cadfe2 100644 --- a/public/css/master.css +++ b/public/css/master.css @@ -115,6 +115,10 @@ div.preview pre.small, div.project pre.small { div.preview pre, div.project pre { background-color: #222; color: #ccc; + display: inline-block; + text-align: left; + padding: 0.2em; + max-width: 90% } div.project pre { @@ -283,13 +287,6 @@ table.skami { color: #FFFFFF; } -div.preview pre, div.project pre { - display: inline-block; - text-align: left; - padding: 0.2em; - max-width: 90% -} - h1 { font-size: 150% } h2 { font-size: 130% } h3 { font-size: 115% } @@ -324,3 +321,32 @@ a { color: #99f; text-decoration: none } + +table.gb_entryform tr td { + border: none; +} + +table.gb_entryform tr td label { + padding-right: 5px; +} + +table.gb_entryform tr td span.text-danger { + padding-left: 5px; + color: rgb(255, 114, 114); +} + +table.gb_entryform tr td textarea, +table.gb_entryform tr td input { + margin-bottom: 5px; +} + +table.gb_entry tr td { + border: solid #FFFFFF; + width: 500px; + vertical-align: top; + padding: 5px; +} + +table.gb_entry { + margin-bottom: 5px; +} diff --git a/public/css/minimal.css b/public/css/minimal.css new file mode 100644 index 0000000..3362057 --- /dev/null +++ b/public/css/minimal.css @@ -0,0 +1,11 @@ +html { + color-scheme: dark; +} + +body { + font-family: sans-serif; + margin: 0px; + margin-left: 10px; + color: #ddd; + background-color: #333; +} diff --git a/resources/views/errors/ratelimit-guestbook.blade.php b/resources/views/errors/ratelimit-guestbook.blade.php new file mode 100644 index 0000000..fa29855 --- /dev/null +++ b/resources/views/errors/ratelimit-guestbook.blade.php @@ -0,0 +1,16 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> +<html lang="en"> +<head> + <title>Error 429: Overclocking Detected!</title> + <link rel="stylesheet" href="{{ URL::asset ('css/minimal.css') }}"/> +</head> + +<body> + <h1>Error 429: Overclocking Detected!</h1> + <hr/> + <p>Whoa there! Your submissions are going at warp speed.</p> + <p>Remember you can only submit an entry <u>once every hour</u>!</p> + <br/> + Click <a href="/guestbook">here</a> to go back to the guestbook. +</body> +</html> diff --git a/resources/views/includes/header.blade.php b/resources/views/includes/header.blade.php index 2951003..53d44e2 100644 --- a/resources/views/includes/header.blade.php +++ b/resources/views/includes/header.blade.php @@ -6,6 +6,7 @@ <a href="/projects/">projects</a> | <a href="/calculators/">calculators</a> | <a href="/computers/">computers</a> | - <a href="/bookmarks/">bookmarks</a> + <a href="/bookmarks/">bookmarks</a> | + <a href="/guestbook/">guestbook</a> </div> </nav> diff --git a/resources/views/pages/guestbook.blade.php b/resources/views/pages/guestbook.blade.php new file mode 100644 index 0000000..f6b758a --- /dev/null +++ b/resources/views/pages/guestbook.blade.php @@ -0,0 +1,53 @@ +@extends('layouts.default') +@section('title', 'guestbook') +@section('content') + <br/> + <form method="POST" action="/guestbook"> + @csrf + <x-honeypot /> + <table class="gb_entryform"> + <tr> + <td> + <label for="name">Name:</label> + </td> + <td> + <input name="name" type="text" id="name" placeholder="John Doe"> + </td> + <td> + <span class="text-danger">{{ $errors->first('name') }}</span> + </td> + </tr> + <tr> + <td> + <label for="message">Message:</label> + </td> + <td> + <textarea name="message" id="message" rows="3"></textarea> + </td> + <td> + <span class="text-danger">{{ $errors->first('message') }}</span> + </td> + </tr> + <tr> + <td colspan="2"> + <button type="submit">Submit</button> + </td> + </tr> + </table> + </form> + <p>You can submit an entry <u>once every hour</u>.</p> + <p>Your IP address <u>will</u> be logged but <u>will not</u> be publically displayed.</p> + <hr/> + @php + $entries = DB::select('SELECT name, timestamp, message FROM guestbook_entries ORDER BY id DESC'); + @endphp + <h1>Entries <small>({{ count($entries) }} total)</small></h1> + @foreach ($entries as $entry) + <table class="gb_entry"><tr><td> + Name: {{ $entry->name }}<br/> + Date: {{ gmdate("H:i:s - Y-m-d", $entry->timestamp) }}<br/><br/> + {{ htmlspecialchars($entry->message) }} + </td></tr></table> + @endforeach +@stop + diff --git a/routes/web.php b/routes/web.php index 055b4e5..6185449 100644 --- a/routes/web.php +++ b/routes/web.php @@ -32,3 +32,12 @@ Route::get('/calculators', function () { Route::get('/computers', function () { return View::make('pages.computers'); }); + +Route::get('/guestbook', 'App\Http\Controllers\GuestbookController@guestbook') + ->name('guestbook'); + +Route::post('/guestbook', 'App\Http\Controllers\GuestbookController@guestbookpost') + ->name('guestbookPost') + ->middleware('rate_limit'); + + |