Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
- 8080:80
strategy:
matrix:
php-versions: [ '8.0', '8.1', '8.2', '8.3', '8.4' ]
php-versions: [ '8.1', '8.2', '8.3', '8.4' ]
composer-options: [ '--prefer-lowest', '']
runs-on: ubuntu-latest
steps:
Expand Down
49 changes: 39 additions & 10 deletions Controller/ReplayController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@
namespace evaisse\SimpleHttpBundle\Controller;

use evaisse\SimpleHttpBundle\Service\Helper;
use evaisse\SimpleHttpBundle\Service\ReplayRequestResolver;
use evaisse\SimpleHttpBundle\Service\ReplayRequestSignature;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;

/**
Expand All @@ -14,19 +20,43 @@
*/
class ReplayController extends AbstractController
{
public function __construct(protected Helper $serviceHelper)
{
public function __construct(
protected Helper $serviceHelper,
private ReplayRequestResolver $replayRequestResolver,
private ReplayRequestSignature $replayRequestSignature,
private bool $debug
) {
}

#[Route('/http-replay', name: 'simple_http.replay_request')]
#[Route('/http-replay', name: 'simple_http.replay_request', methods: ['POST'])]
public function replayRequestAction(Request $request): Response
{
$request = json_decode($request->request->get('request'));
if (!$this->debug || !$this->replayRequestResolver->isAvailable()) {
throw new NotFoundHttpException('Replay is only available with the profiler enabled.');
}

$service = $this->serviceHelper->prepare($request->method, $request->uri);
$service->getRequest()->setContent($request->content);
$token = $request->request->getString('token');
$signature = $request->request->getString('_token');
$callIndex = filter_var(
$request->request->get('callIndex'),
FILTER_VALIDATE_INT,
['options' => ['min_range' => 0]]
);

foreach ($request->headers as $header) {
if ('' === $token || false === $callIndex) {
throw new BadRequestHttpException('Invalid replay request.');
}

if (!$this->replayRequestSignature->isValid($token, $callIndex, $signature)) {
throw new AccessDeniedHttpException('Invalid replay token.');
}

$storedRequest = $this->replayRequestResolver->resolve($token, $callIndex);

$service = $this->serviceHelper->prepare($storedRequest['method'], $storedRequest['uri']);
$service->getRequest()->setContent($storedRequest['content']);

foreach ($storedRequest['headers'] as $header) {
if (fnmatch('*:*', $header)) {
list($headerName, $headerValue) = explode(':', $header, 2);
$headerName = trim($headerName);
Expand All @@ -35,15 +65,14 @@ public function replayRequestAction(Request $request): Response
}
}

foreach ($request->cookies as $cookieName => $cookieValue) {
foreach ($storedRequest['cookies'] as $cookieName => $cookieValue) {
$service->getRequest()->cookies->set($cookieName, $cookieValue);
}


$this->serviceHelper->execute([
$service,
]);

return new Response('ok');
return new JsonResponse(['status' => 'ok']);
}
}
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ SimpleHttpBundle

### While Still maintained, I recommand you to replace this bundle with the [symfony/http-client](https://github.com/symfony/http-client) with has better support and a handy plug-in support.

A symfony2/3/4/5 http client bundle built on the httpfoundation component (instead of guzzle), using cURL as engine.
A Symfony 5.4/6.x HTTP client bundle built on the HttpFoundation component (instead of guzzle), using cURL as engine.

Requirements
======

- PHP 8.1+
- Symfony 5.4 or 6.x

[![Coverage Status](https://coveralls.io/repos/evaisse/SimpleHttpBundle/badge.svg?branch=master)](https://coveralls.io/r/evaisse/SimpleHttpBundle?branch=master)

Expand Down Expand Up @@ -78,6 +84,8 @@ or


You can now resend an http call directly from the profiler debug panel

Replay requests are now resolved server-side from the profiler storage. The browser only sends the profiler token and request index, not the serialized request payload.


Panels details
Expand Down Expand Up @@ -200,4 +208,3 @@ $http->execute([
```



13 changes: 10 additions & 3 deletions Resources/config/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ services:
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
public: false

evaisse\SimpleHttpBundle\Controller\ReplayController: ~
evaisse\SimpleHttpBundle\Controller\ReplayController:
arguments:
$debug: '%kernel.debug%'
evaisse\SimpleHttpBundle\Service\ReplayRequestResolver: ~
evaisse\SimpleHttpBundle\Service\ReplayRequestSignature:
arguments:
$secret: '%kernel.secret%'
evaisse\SimpleHttpBundle\Http\Kernel: ~
evaisse\SimpleHttpBundle\Service\Helper: ~

Expand Down Expand Up @@ -38,6 +44,7 @@ services:
- { name: data_collector, template: "@SimpleHttp/Collector/profiler", id: "simplehttpprofiler" }

evaisse\SimpleHttpBundle\Twig\Extension:
arguments: ["@twig.loader"]
arguments:
$loader: "@twig.loader"
tags:
- { name: twig.extension }
- { name: twig.extension }
8 changes: 4 additions & 4 deletions Resources/public/simple-http-bundle.css
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ a.http-call__index:hover {
display: inline;
}

button[data-simple-http-replay] {
button[data-simple-http-replay-token] {
border: 1px solid #CCD;
background: #EEF;
color: #777;
Expand All @@ -216,13 +216,13 @@ button[data-simple-http-replay] {
}


button[data-simple-http-replay] img {
button[data-simple-http-replay-token] img {
float: left;
zoom: 75%;
margin-right: 10px;
}

button[data-simple-http-replay] img.rotating {
button[data-simple-http-replay-token] img.rotating {
-webkit-animation: rotating 2s linear infinite;
}

Expand Down Expand Up @@ -266,4 +266,4 @@ button[data-simple-http-replay] img.rotating {
overflow-x: hidden;
text-decoration: none;
border-bottom: 0;
}
}
21 changes: 13 additions & 8 deletions Resources/public/simple-http-bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
*
*/
(function ($) {
function replayRequest(url, request, callback) {
window.console && console.log('Replay request :', request);

function replayRequest(url, token, callIndex, signature, callback) {
$.post({
url: url,
data: {
request: JSON.stringify(request)
token: token,
callIndex: callIndex,
_token: signature
}
}).done(function (data, success, xhr) {
// Wait 1 second to be sure the profiler has been generated and can be accessed
Expand Down Expand Up @@ -69,7 +69,7 @@
filterByHost(event.target.value);
});

$(element).find('[data-simple-http-replay]').click(function (replay, i) {
$(element).find('[data-simple-http-replay-token]').click(function (replay, i) {

var $panel = $(this).closest('.http-call'),
$butt = $(this);
Expand All @@ -78,7 +78,12 @@

$butt.prop('disabled', true);

replayRequest($(this).data('simpleHttpReplayUrl'), $(this).data('simpleHttpReplay'), function (error, block) {
replayRequest(
$(this).data('simpleHttpReplayUrl'),
$(this).data('simpleHttpReplayToken'),
$(this).data('simpleHttpReplayIndex'),
$(this).data('simpleHttpReplaySignature'),
function (error, block) {
if (error) {
alert('Error on replay request');
}
Expand All @@ -90,7 +95,8 @@
.appendTo($panel.find('.http-call__replay-calls:first'));
$(document).trigger('setup', [block]); // init events
$buttImg.removeClass('rotating');
});
}
);

});

Expand All @@ -105,4 +111,3 @@
})(jQuery);



4 changes: 3 additions & 1 deletion Resources/views/Collector/partials/call.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@
<div class="http-call__replay-calls"></div>
<div class="http-call__replay-action">
<button data-simple-http-replay-url="{{ path("simple_http.replay_request") }}"
data-simple-http-replay="{{ call.request|json_encode }}">
data-simple-http-replay-token="{{ token }}"
data-simple-http-replay-index="{{ index }}"
data-simple-http-replay-signature="{{ simple_http_replay_token(token, index) }}">
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABQ0lEQVQ4T6XSv0tXcRTG8ZeKS4ODOvRDwX/AxZCgwc1QWoQGXXUSQURQQXB0KJRoEMJdELcEwQbRSfLHIA3ODQUm2uDUlMWBc+Fyu/JF/Cz33vM5z/ue85zT5IGnqYH+OeZwjE1cVvMbAYawm6LfWML7MqQRoBsTeIPeFK5ivoAUgFa8wijaMFIpNfIW8Dbjw/gc73HRhW305eUtnuFnjT8fMYkveFkA4tmBqwTG9zTWagA9+JbxTvwqWniBowhkfwMYrwBa8Cd/FOLQnBSAKOcwy36CZkQr5bOBg5xC+BQtnxWAx7jI7HD+R035M/hQ8qkdN+UxnqAfYdRUDeBpggvNaUyrDHiNnRQu4h3+VkCfcI0t7Icn1UVaydUN3TlCsI7vd6183SbOYhmPUjSIvfsAIjfGNJaexO5/vS/grvz/4v8AYOI4K1/F6HgAAAAASUVORK5CYII=" />
Replay
</button>
Expand Down
2 changes: 1 addition & 1 deletion Resources/views/Collector/profiler.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@

{% for index, call in collector.calls %}
<a name="request{{ index }}"></a>
{% include '@SimpleHttp/Collector/partials/call.html.twig' with { 'call': call } %}
{% include '@SimpleHttp/Collector/partials/call.html.twig' with { 'call': call, 'index': index, 'token': token } %}
{% endfor %}

<script type="text/javascript" src="{{ asset('bundles/simplehttp/libs/jquery/jquery-1.12.1.js') }}"></script>
Expand Down
107 changes: 107 additions & 0 deletions Service/ReplayRequestResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

namespace evaisse\SimpleHttpBundle\Service;

use evaisse\SimpleHttpBundle\DataCollector\ProfilerDataCollector;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Profiler\Profiler;
use Symfony\Component\HttpKernel\Profiler\Profile;

class ReplayRequestResolver
{
public function __construct(private ?Profiler $profiler = null)
{
}

public function isAvailable(): bool
{
return null !== $this->profiler;
}

/**
* @return array{
* method: string,
* uri: string,
* content: string,
* headers: string[],
* cookies: array<string, mixed>
* }
*/
public function resolve(string $token, int $callIndex): array
{
if (!$this->profiler) {
throw new NotFoundHttpException('Profiler is not available.');
}

$profile = $this->profiler->loadProfile($token);
if (!$profile instanceof Profile) {
throw new NotFoundHttpException('Profiler token not found.');
}

if (!$profile->hasCollector('simplehttpprofiler')) {
throw new NotFoundHttpException('Simple HTTP collector not found.');
}

$collector = $profile->getCollector('simplehttpprofiler');
if (!$collector instanceof ProfilerDataCollector) {
throw new NotFoundHttpException('Simple HTTP collector is invalid.');
}

$calls = $collector->getCalls();
if (!array_key_exists($callIndex, $calls) || !is_array($calls[$callIndex])) {
throw new NotFoundHttpException('HTTP call not found.');
}

$request = $calls[$callIndex]['request'] ?? null;
if (!is_array($request)) {
throw new NotFoundHttpException('Stored request is invalid.');
}

$method = isset($request['method']) && is_string($request['method']) ? $request['method'] : '';
$uri = $this->resolveUri($request);

if ('' === $method || '' === $uri) {
throw new NotFoundHttpException('Stored request is incomplete.');
}

return [
'method' => $method,
'uri' => $uri,
'content' => isset($request['content']) && is_string($request['content']) ? $request['content'] : '',
'headers' => $this->normalizeHeaders($request['headers'] ?? []),
'cookies' => is_array($request['cookies'] ?? null) ? $request['cookies'] : [],
];
}

/**
* @param mixed $headers
* @return string[]
*/
private function normalizeHeaders(mixed $headers): array
{
if (!is_array($headers)) {
return [];
}

return array_values(array_filter($headers, static fn (mixed $header): bool => is_string($header)));
}

/**
* @param array<string, mixed> $request
*/
private function resolveUri(array $request): string
{
if (isset($request['uri']) && is_string($request['uri']) && '' !== $request['uri']) {
return $request['uri'];
}

$schemeAndHttpHost = $request['schemeAndHttpHost'] ?? null;
$requestUri = $request['requestUri'] ?? null;

if (is_string($schemeAndHttpHost) && is_string($requestUri) && '' !== $schemeAndHttpHost && '' !== $requestUri) {
return $schemeAndHttpHost . $requestUri;
}

return '';
}
}
Loading
Loading