Fix ladl.link transfer QR redirect loop causing 500 errors.
Deploy Ladill Link / deploy (push) Successful in 1m1s
Deploy Ladill Link / deploy (push) Successful in 1m1s
Proxy transfer paths directly to transfer.ladill.com and stop following ladl.link redirects server-side so recipient links resolve. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -8,45 +8,40 @@ use Illuminate\Support\Facades\Http;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Proxies ladl.link ecosystem paths to the platform QR resolver (/q/* on ladill.com).
|
||||
* The browser stays on ladl.link; resolution happens server-side.
|
||||
* Proxies ladl.link ecosystem paths to the platform QR resolver (/q/* on ladill.com)
|
||||
* or directly to satellite apps (e.g. transfer.ladill.com). The browser stays on
|
||||
* ladl.link; resolution happens server-side without redirect loops.
|
||||
*/
|
||||
class LinkPlatformProxy
|
||||
{
|
||||
public const INTERNAL_HEADER = 'X-Ladill-Internal';
|
||||
|
||||
public function forward(Request $request, string $slug, ?string $path = null): Response
|
||||
{
|
||||
$normalizedPath = $path !== null ? ltrim($path, '/') : '';
|
||||
|
||||
if ($normalizedPath === 'transfer' || str_starts_with($normalizedPath, 'transfer/')) {
|
||||
return $this->fetch(
|
||||
$request,
|
||||
rtrim($this->transferBaseUrl(), '/').'/q/'.$slug.'/'.$normalizedPath,
|
||||
);
|
||||
}
|
||||
|
||||
$platform = rtrim((string) config('app.platform_url', 'https://ladill.com'), '/');
|
||||
$target = $platform.'/q/'.$slug;
|
||||
if ($path !== null && $path !== '') {
|
||||
$target .= '/'.ltrim($path, '/');
|
||||
}
|
||||
if ($qs = $request->getQueryString()) {
|
||||
$target .= '?'.$qs;
|
||||
if ($normalizedPath !== '') {
|
||||
$target .= '/'.$normalizedPath;
|
||||
}
|
||||
|
||||
$method = strtoupper($request->method());
|
||||
$followRedirects = $method === 'GET' || $method === 'HEAD';
|
||||
$client = Http::withOptions([
|
||||
'allow_redirects' => $followRedirects ? ['max' => 5] : false,
|
||||
])->timeout(15);
|
||||
|
||||
$pending = match ($method) {
|
||||
'POST' => $client->withHeaders($this->forwardHeaders($request))->asForm()->post($target, $request->post()),
|
||||
'PATCH' => $client->withHeaders($this->forwardHeaders($request))->patch($target, $request->all()),
|
||||
'DELETE' => $client->withHeaders($this->forwardHeaders($request))->delete($target),
|
||||
default => $client->withHeaders($this->forwardHeaders($request))->get($target),
|
||||
};
|
||||
|
||||
$upstream = $pending->toPsrResponse();
|
||||
$response = new Response(
|
||||
(string) $upstream->getBody(),
|
||||
$upstream->getStatusCode(),
|
||||
$this->sanitizeHeaders($upstream->getHeaders())
|
||||
);
|
||||
$response = $this->fetch($request, $target);
|
||||
|
||||
if ($response->isRedirection()) {
|
||||
$location = $response->headers->get('Location');
|
||||
if (is_string($location)) {
|
||||
if ($followUp = $this->followLadillLinkRedirect($request, $slug, $location)) {
|
||||
return $followUp;
|
||||
}
|
||||
|
||||
$response->headers->set('Location', $this->rewriteLocation($location, $slug));
|
||||
}
|
||||
}
|
||||
@@ -54,6 +49,62 @@ class LinkPlatformProxy
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function fetch(Request $request, string $target): Response
|
||||
{
|
||||
if ($qs = $request->getQueryString()) {
|
||||
$target .= (str_contains($target, '?') ? '&' : '?').$qs;
|
||||
}
|
||||
|
||||
$method = strtoupper($request->method());
|
||||
$client = Http::withOptions(['allow_redirects' => false])
|
||||
->timeout(15)
|
||||
->withHeaders($this->forwardHeaders($request));
|
||||
|
||||
$pending = match ($method) {
|
||||
'POST' => $client->asForm()->post($target, $request->post()),
|
||||
'PATCH' => $client->patch($target, $request->all()),
|
||||
'DELETE' => $client->delete($target),
|
||||
default => $client->get($target),
|
||||
};
|
||||
|
||||
$upstream = $pending->toPsrResponse();
|
||||
|
||||
return new Response(
|
||||
(string) $upstream->getBody(),
|
||||
$upstream->getStatusCode(),
|
||||
$this->sanitizeHeaders($upstream->getHeaders()),
|
||||
);
|
||||
}
|
||||
|
||||
private function followLadillLinkRedirect(Request $request, string $slug, string $location): ?Response
|
||||
{
|
||||
$parsed = parse_url($location);
|
||||
if (! is_array($parsed)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$host = strtolower((string) ($parsed['host'] ?? ''));
|
||||
$linkHost = strtolower((string) parse_url(LadillLink::baseUrl(), PHP_URL_HOST));
|
||||
if ($host !== $linkHost) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$path = ltrim((string) ($parsed['path'] ?? ''), '/');
|
||||
$slugPrefix = $slug.'/';
|
||||
if (! str_starts_with($path, $slugPrefix)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->forward($request, $slug, substr($path, strlen($slugPrefix)));
|
||||
}
|
||||
|
||||
private function transferBaseUrl(): string
|
||||
{
|
||||
$domain = (string) config('app.transfer_domain', 'transfer.ladill.com');
|
||||
|
||||
return 'https://'.$domain;
|
||||
}
|
||||
|
||||
/** @return array<string, string> */
|
||||
private function forwardHeaders(Request $request): array
|
||||
{
|
||||
@@ -62,7 +113,7 @@ class LinkPlatformProxy
|
||||
'Accept-Language' => $request->header('Accept-Language'),
|
||||
'User-Agent' => $request->header('User-Agent'),
|
||||
'Referer' => $request->header('Referer'),
|
||||
'X-Ladill-Internal' => '1',
|
||||
self::INTERNAL_HEADER => '1',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -87,9 +138,15 @@ class LinkPlatformProxy
|
||||
|
||||
if (str_starts_with($location, $prefix)) {
|
||||
$suffix = substr($location, strlen($prefix));
|
||||
$rewritten = LadillLink::url($slug).$suffix;
|
||||
|
||||
return $rewritten;
|
||||
return LadillLink::url($slug).$suffix;
|
||||
}
|
||||
|
||||
$transfer = rtrim($this->transferBaseUrl(), '/').'/q/'.$slug;
|
||||
if (str_starts_with($location, $transfer)) {
|
||||
$suffix = substr($location, strlen($transfer));
|
||||
|
||||
return LadillLink::url($slug).$suffix;
|
||||
}
|
||||
|
||||
return $location;
|
||||
|
||||
@@ -129,6 +129,7 @@ return [
|
||||
'auth_domain' => env('AUTH_DOMAIN', 'auth.'.(parse_url((string) env('PLATFORM_URL', 'https://ladill.com'), PHP_URL_HOST) ?: 'ladill.com')),
|
||||
'account_domain' => env('ACCOUNT_DOMAIN', 'account.'.(parse_url((string) env('PLATFORM_URL', 'https://ladill.com'), PHP_URL_HOST) ?: 'ladill.com')),
|
||||
'link_domain' => env('LINK_DOMAIN', parse_url((string) env('APP_URL', 'https://link.ladill.com'), PHP_URL_HOST) ?: 'link.ladill.com'),
|
||||
'transfer_domain' => env('TRANSFER_DOMAIN', 'transfer.ladill.com'),
|
||||
'servers_domain' => env('SERVERS_DOMAIN', 'servers.'.(parse_url((string) env('PLATFORM_URL', 'https://ladill.com'), PHP_URL_HOST) ?: 'ladill.com')),
|
||||
'domains_domain' => env('DOMAINS_DOMAIN', 'domains.'.(parse_url((string) env('PLATFORM_URL', 'https://ladill.com'), PHP_URL_HOST) ?: 'ladill.com')),
|
||||
|
||||
|
||||
@@ -65,4 +65,34 @@ class LinkRedirectTest extends TestCase
|
||||
&& $request->hasHeader('X-Ladill-Internal', '1');
|
||||
});
|
||||
}
|
||||
|
||||
public function test_transfer_qr_proxies_to_transfer_app_without_redirect_loop(): void
|
||||
{
|
||||
$user = $this->user();
|
||||
ShortLink::create([
|
||||
'user_id' => $user->id,
|
||||
'slug' => 'mo4o0v1t',
|
||||
'source_app' => 'transfer',
|
||||
'source_kind' => 'qr',
|
||||
'is_managed_here' => false,
|
||||
'destination_url' => 'https://ladl.link/mo4o0v1t',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://ladill.com/q/mo4o0v1t' => Http::response('', 302, [
|
||||
'Location' => 'https://ladl.link/mo4o0v1t/transfer',
|
||||
]),
|
||||
'https://transfer.ladill.com/q/mo4o0v1t/transfer' => Http::response('<html>Transfer files</html>', 200),
|
||||
]);
|
||||
|
||||
$this->get('https://ladl.link/mo4o0v1t')
|
||||
->assertOk()
|
||||
->assertSee('Transfer files');
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
return $request->url() === 'https://transfer.ladill.com/q/mo4o0v1t/transfer'
|
||||
&& $request->hasHeader('X-Ladill-Internal', '1');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user