diff --git a/.env.example b/.env.example index 9fd98981..9bb7bb1e 100644 --- a/.env.example +++ b/.env.example @@ -50,3 +50,6 @@ MYKAD_READER_PORT=17321 MYKAD_READER_ENABLED_CAWANGAN='' INTERNAL_NOTIFICATION_SECRET= + +# Separate password for /logs (username: logs). Empty disables access. +LOGVIEWER_PASSWORD= diff --git a/app/Http/Middleware/LogViewerPassword.php b/app/Http/Middleware/LogViewerPassword.php new file mode 100644 index 00000000..c02c7e93 --- /dev/null +++ b/app/Http/Middleware/LogViewerPassword.php @@ -0,0 +1,32 @@ +header('Cache-Control', 'no-store'); + } + + $provided = $request->getPassword(); + if ($request->getUser() !== 'logs' || !is_string($provided) || + !hash_equals(hash('sha256', $password), hash('sha256', $provided))) { + return response('Kata laluan diperlukan untuk mengakses log.', 401, [ + 'WWW-Authenticate' => 'Basic realm="Log Viewer", charset="UTF-8"', + 'Cache-Control' => 'no-store', + ]); + } + + $response = $next($request); + $response->headers->set('Cache-Control', 'no-store'); + + return $response; + } +} diff --git a/config/logviewer.php b/config/logviewer.php index 90c2a6f5..5746448d 100644 --- a/config/logviewer.php +++ b/config/logviewer.php @@ -1,6 +1,7 @@ env('LOGVIEWER_PASSWORD'), /* |-------------------------------------------------------------------------- | Pattern and storage path settings diff --git a/routes/web.php b/routes/web.php index 338f3ebf..1c0574b1 100644 --- a/routes/web.php +++ b/routes/web.php @@ -63,7 +63,8 @@ Route::get('test', function () { return "event test je"; }); -Route::get('logs', '\Rap2hpoutre\LaravelLogViewer\LogViewerController@index'); +Route::get('logs', '\Rap2hpoutre\LaravelLogViewer\LogViewerController@index') + ->middleware(['throttle:10,1', \App\Http\Middleware\LogViewerPassword::class]); Route::get('change-password', ['as'=>'user.password','uses'=>'ChangePasswordController@index']); Route::post('change-password', ['as'=>'change.password','uses'=>'ChangePasswordController@store']); diff --git a/tests/Feature/LogViewerPasswordTest.php b/tests/Feature/LogViewerPasswordTest.php new file mode 100644 index 00000000..43e5991d --- /dev/null +++ b/tests/Feature/LogViewerPasswordTest.php @@ -0,0 +1,52 @@ + null]); + $response = $this->checkAccess(); + $this->assertSame(503, $response->getStatusCode()); + } + + public function test_missing_or_incorrect_credentials_are_rejected() + { + config(['logviewer.password' => 'test-only-secret']); + foreach ([[], ['PHP_AUTH_USER' => 'logs', 'PHP_AUTH_PW' => 'wrong'], + ['PHP_AUTH_USER' => 'other', 'PHP_AUTH_PW' => 'test-only-secret']] as $server) { + $response = $this->checkAccess($server); + $this->assertSame(401, $response->getStatusCode()); + $this->assertStringContainsString('Basic realm=', $response->headers->get('WWW-Authenticate')); + } + } + + public function test_correct_credentials_allow_access_without_caching() + { + config(['logviewer.password' => 'test-only-secret']); + $response = $this->checkAccess(['PHP_AUTH_USER' => 'logs', 'PHP_AUTH_PW' => 'test-only-secret']); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('allowed', $response->getContent()); + $this->assertStringContainsString('no-store', $response->headers->get('Cache-Control')); + } + + public function test_logs_route_has_password_and_throttle_middleware() + { + $route = app('router')->getRoutes()->match(Request::create('/logs')); + $this->assertContains(LogViewerPassword::class, $route->gatherMiddleware()); + $this->assertContains('throttle:10,1', $route->gatherMiddleware()); + } + + private function checkAccess(array $server = []) + { + $request = Request::create('/logs?dl=example', 'GET', [], [], [], $server); + return (new LogViewerPassword)->handle($request, function () { + return response('allowed'); + }); + } +}