diff --git a/README.md b/README.md index a9c56c2..d190cbd 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ It runs without installation on multiple platforms via docker and supports [symf - [Monolog server](#4-monolog-server) - [Inspector](#5-compatible-with-inspector-reports) - [Spatie Ray debug tool](#6-spatie-ray-debug-tool) + - [HTTP Requests dump server](#7-http-requests-dump-server) 2. [Installation](#installation) - [Docker image](#docker-image) 3. [Configuration](#configuration) @@ -213,6 +214,9 @@ Json, Xml, Carbon, File, Table, Image, Html, Text, Notifications, Phpinfo, Excep Show jobs, Show cache, Model, Show views, Markdown, Collections, Env, Response, Request, Ban, Charles, Remove, Hide/Show events, Application log, Show Http client requests, Mailable +## 7. HTTP Requests dump server +Buggregator can receive HTTP requests and store them for inspection. + ### Laravel settings Please make sure `ray.php` config published to the project root. diff --git a/app/Application/Listeners/Request/SendRequestDebugToConsole.php b/app/Application/Listeners/Request/SendRequestDebugToConsole.php index 606ea6a..a54eab7 100644 --- a/app/Application/Listeners/Request/SendRequestDebugToConsole.php +++ b/app/Application/Listeners/Request/SendRequestDebugToConsole.php @@ -33,6 +33,7 @@ public function handle($event): void 'POST' => 'blue', 'PUT' => 'yellow', 'DELETE' => 'red', + 'HEAD' => 'white', }, 'responseColor' => match (true) { $statusCode >= 500 => 'red', diff --git a/app/Modules/Events/Application/Resources/EventResource.php b/app/Modules/Events/Application/Resources/EventResource.php index b9a122e..736267a 100644 --- a/app/Modules/Events/Application/Resources/EventResource.php +++ b/app/Modules/Events/Application/Resources/EventResource.php @@ -8,6 +8,8 @@ class EventResource extends JsonResource { + public bool $preserveKeys = true; + public function toArray($request) { return [ diff --git a/app/Modules/HttpDump/Interfaces/Http/Controllers/StoreEventAction.php b/app/Modules/HttpDump/Interfaces/Http/Controllers/StoreEventAction.php new file mode 100644 index 0000000..47ba5c6 --- /dev/null +++ b/app/Modules/HttpDump/Interfaces/Http/Controllers/StoreEventAction.php @@ -0,0 +1,82 @@ +ask(new FindProjectByName('default')); + $projectId = $project->getId(); + + $data = $this->prepareRequest($request); + $commands->dispatch( + new HandleReceivedEvent( + (int) $projectId, + 'httpdump', + $data, + true + ) + ); + } + + private function prepareRequest(Request $request) + { + return [ + 'received_at' => now()->toDateTimeString(), + 'request' => [ + 'method' => $request->getMethod(), + 'uri' => $request->getRequestUri(), + 'headers' => $request->headers->all(), + 'body' => $request->getContent(), + 'query' => $request->query->all(), + 'post' => $this->getPostData($request), + ], + ]; + } + + private function getPostData(Request $request): array + { + $contentType = current(Arr::get($request->headers->all(), 'content-type', [])) ?: 'no content type'; + + if ($contentType === 'application/x-www-form-urlencoded') { + return $request->request->all(); + } + + if ($contentType === 'application/json') { + return json_decode($request->getContent(), true); + } + + if (str_starts_with($contentType, 'multipart/form-data') && $request->allFiles() !== []) { + return array_map(function (UploadedFile $file) { + return [ + 'originalName' => $file->getClientOriginalName(), + 'mime' => $file->getClientMimeType(), + 'size' => $file->getSize(), + ]; + }, collect($request->files->all())->flatten()->toArray()); + } + + return []; + } +} diff --git a/app/Modules/HttpDump/ServiceProvider.php b/app/Modules/HttpDump/ServiceProvider.php new file mode 100644 index 0000000..03ee1a4 --- /dev/null +++ b/app/Modules/HttpDump/ServiceProvider.php @@ -0,0 +1,22 @@ +app->bind(EventHandlerContract::class, function () { + return new EventHandler($this->app, []); + }); + } + + public function boot() + { + $this->loadViewsFrom(__DIR__.'/resources/views', 'httpdump'); + } +} diff --git a/config/app.php b/config/app.php index a857b97..84c9bed 100644 --- a/config/app.php +++ b/config/app.php @@ -193,6 +193,7 @@ Modules\Ray\ServiceProvider::class, Modules\Sentry\ServiceProvider::class, Modules\User\ServiceProvider::class, + Modules\HttpDump\ServiceProvider::class, /* * Application Service Providers... diff --git a/config/server.php b/config/server.php index 88eed2d..e5d8eba 100644 --- a/config/server.php +++ b/config/server.php @@ -26,6 +26,12 @@ ], ], ], + 'httpdump' => [ + 'http' => [ + 'index' => 'HttpDump/Index', + 'show' => 'HttpDump/Show', + ], + ], 'sentry' => [ 'http' => [ 'index' => 'Sentry/Index', diff --git a/resources/js/Components/HttpDump/Event.vue b/resources/js/Components/HttpDump/Event.vue new file mode 100644 index 0000000..724d407 --- /dev/null +++ b/resources/js/Components/HttpDump/Event.vue @@ -0,0 +1,22 @@ + + + diff --git a/resources/js/Components/HttpDump/List/Item.vue b/resources/js/Components/HttpDump/List/Item.vue new file mode 100644 index 0000000..fbc218d --- /dev/null +++ b/resources/js/Components/HttpDump/List/Item.vue @@ -0,0 +1,19 @@ + + + diff --git a/resources/js/Components/HttpDump/Show/Headers.vue b/resources/js/Components/HttpDump/Show/Headers.vue new file mode 100644 index 0000000..19b0e10 --- /dev/null +++ b/resources/js/Components/HttpDump/Show/Headers.vue @@ -0,0 +1,24 @@ + + + diff --git a/resources/js/Components/HttpDump/Show/PostData.vue b/resources/js/Components/HttpDump/Show/PostData.vue new file mode 100644 index 0000000..9227590 --- /dev/null +++ b/resources/js/Components/HttpDump/Show/PostData.vue @@ -0,0 +1,24 @@ + + + diff --git a/resources/js/Components/HttpDump/Show/QueryParameters.vue b/resources/js/Components/HttpDump/Show/QueryParameters.vue new file mode 100644 index 0000000..5a687a3 --- /dev/null +++ b/resources/js/Components/HttpDump/Show/QueryParameters.vue @@ -0,0 +1,24 @@ + + + diff --git a/resources/js/Components/HttpDump/Show/RequestBody.vue b/resources/js/Components/HttpDump/Show/RequestBody.vue new file mode 100644 index 0000000..6b9e7a7 --- /dev/null +++ b/resources/js/Components/HttpDump/Show/RequestBody.vue @@ -0,0 +1,21 @@ + + + diff --git a/resources/js/Components/Layout/Sidebar/Left.vue b/resources/js/Components/Layout/Sidebar/Left.vue index be04ad6..da4d4e6 100644 --- a/resources/js/Components/Layout/Sidebar/Left.vue +++ b/resources/js/Components/Layout/Sidebar/Left.vue @@ -60,6 +60,13 @@ export default { icon: '', supIcon: computed(() => this.unReadEvents.filter(i => i === 'inspector').length > 0), }, + { + href: route('events.type', 'httpdump'), + title: 'HTTP Dump', + state: (url) => this.$page.url.startsWith(url), + icon: '', + supIcon: computed(() => this.unReadEvents.filter(i => i === 'httpdump').length > 0), + }, // { // href: route('performance'), // title: 'Performance', diff --git a/resources/js/EventFactory.js b/resources/js/EventFactory.js index bf7d0c4..e728b98 100644 --- a/resources/js/EventFactory.js +++ b/resources/js/EventFactory.js @@ -6,6 +6,7 @@ import SmtpEvent from "./Smtp/event"; import VarDumpEvent from "./VarDump/event"; import InspectorEvent from "./Inspector/event"; import SentryTransactionEvent from "./SentryTransaction/event"; +import HttpDumpEvent from "./HttpDump/event"; import {store} from "./store"; const eventTypes = { @@ -21,7 +22,8 @@ const eventTypes = { smtp: json => new SmtpEvent(json.payload, json.uuid, json.timestamp), inspector: json => new InspectorEvent(json.payload, json.uuid, json.timestamp), sentrytransaction: json => new SentryTransactionEvent(json.payload, json.uuid, json.timestamp, json.projectId, json.transactionId), - 'var-dump': json => new VarDumpEvent(json.payload, json.uuid, json.timestamp) + 'var-dump': json => new VarDumpEvent(json.payload, json.uuid, json.timestamp), + httpdump: json => new HttpDumpEvent(json.payload, json.uuid, json.timestamp) } export default { @@ -48,6 +50,8 @@ export default { store.commit('sentryTransaction/pushEvent', event) } else if (event instanceof InspectorEvent) { store.commit('inspector/pushEvent', event) + } else if (event instanceof HttpDumpEvent) { + store.commit('httpdump/pushEvent', event) } store.commit('pushUnreadEvent', event.app) store.commit('pushEvent', event) @@ -66,6 +70,9 @@ export default { if (e.payload.type === 'inspector') { store.commit('inspector/clearEvents') } + if (e.payload.type === 'httpdump') { + store.commit('httpdump/clearEvents') + } store.commit('clearEvents', e.payload.type) }) diff --git a/resources/js/HttpDump/event.js b/resources/js/HttpDump/event.js new file mode 100644 index 0000000..30de49c --- /dev/null +++ b/resources/js/HttpDump/event.js @@ -0,0 +1,15 @@ +import {Event} from "@/Event" + +export default class extends Event{ + labels = [] + app = 'httpdump' + + constructor(event, id, timestamp) { + super(event, id, timestamp) + this.labels.push(this.event.request.method) + } + + get type() { + return 'httpdump' + } +} diff --git a/resources/js/Pages/Events.vue b/resources/js/Pages/Events.vue index 50aef17..fc1829b 100644 --- a/resources/js/Pages/Events.vue +++ b/resources/js/Pages/Events.vue @@ -45,6 +45,7 @@ import VarDumpComponent from "@/Components/VarDump/Event" import MonologComponent from "@/Components/Monolog/Event" import InspectorComponent from "@/Components/Inspector/Event" import SentryTransactionComponent from "@/Components/SentryTransaction/Event" +import HttpDumpComponent from "@/Components/HttpDump/Event" import RayEvent from "@/Ray/event" import SentryEvent from "@/Sentry/event" @@ -54,6 +55,7 @@ import SmtpEvent from "@/Smtp/event" import VarDumpEvent from "@/VarDump/event" import MonologEvent from "@/Monolog/event" import InspectorEvent from "@/Inspector/event" +import HttpDumpEvent from "@/HttpDump/event" import EventFactory from "@/EventFactory" export default { @@ -65,7 +67,7 @@ export default { Screens, Head, Link, RayEventComponent, SentryEventComponent, SlackEventComponent, SmtpEventComponent, VarDumpComponent, MonologComponent, InspectorComponent, - SentryTransactionComponent, + SentryTransactionComponent, HttpDumpComponent }, computed: { @@ -92,6 +94,8 @@ export default { return 'InspectorComponent' } else if (event instanceof SentryTransactionEvent) { return 'SentryTransactionComponent' + } else if (event instanceof HttpDumpEvent) { + return 'HttpDumpComponent' } return 'RayEventComponent' diff --git a/resources/js/Pages/HttpDump/Index.vue b/resources/js/Pages/HttpDump/Index.vue new file mode 100644 index 0000000..d8812c7 --- /dev/null +++ b/resources/js/Pages/HttpDump/Index.vue @@ -0,0 +1,63 @@ + + + diff --git a/resources/js/Pages/HttpDump/Show.vue b/resources/js/Pages/HttpDump/Show.vue new file mode 100644 index 0000000..74640b7 --- /dev/null +++ b/resources/js/Pages/HttpDump/Show.vue @@ -0,0 +1,73 @@ + + + diff --git a/resources/js/store.js b/resources/js/store.js index a2bf609..487600a 100644 --- a/resources/js/store.js +++ b/resources/js/store.js @@ -79,7 +79,36 @@ const smtp = { if (state.events.find(e => event.uuid == e.uuid)) { return } - + + state.events.unshift(event) + }, + openEvent(state, event) { + state.event = event + }, + deleteEvent(state, event) { + state.events = state.events.filter(e => event.uuid == e.uuid) + }, + closeEvent(state) { + state.event = null + } + } +} + +const httpdump = { + namespaced: true, + state: () => ({ + events: [], + event: null + }), + mutations: { + clearEvents(state) { + state.events = [] + }, + pushEvent(state, event) { + if (state.events.find(e => event.uuid == e.uuid)) { + return + } + state.events.unshift(event) }, openEvent(state, event) { @@ -182,7 +211,7 @@ const inspector = { export const store = createStore({ modules: { - ws, smtp, sentry, sentryTransaction, terminal, inspector, theme + ws, smtp, sentry, sentryTransaction, terminal, inspector, theme, httpdump }, state() { diff --git a/tests/Feature/Http/HttpDumpControllerTest.php b/tests/Feature/Http/HttpDumpControllerTest.php new file mode 100644 index 0000000..90cdf72 --- /dev/null +++ b/tests/Feature/Http/HttpDumpControllerTest.php @@ -0,0 +1,26 @@ +call('POST', route('httpdump.event.store', 1), content: ['now' => $value]); + + /** @var \Modules\Events\Domain\Event $event */ + $event = $this->getRepositoryFor(Event::class)->findAll()->first(); + + $this->assertSame('httpdump', $event->getType()); + $this->assertSame(['now' => $value], $event->getPayload()->toArray()['request']['body']); + $this->assertSame('POST', $event->getPayload()->toArray()['request']['method']); + + return $event; + } +}