Repository navigation
feat: add resolver and redirect controls for source requests - #229
Conversation
Consumers that validate a source endpoint before a migration need every request to reach the address they approved. Target::call followed redirects to any host and let libcurl use every protocol it was built with, so the request could end up somewhere the endpoint check never saw. Requests now use http and https only, for the request and any redirect. Redirects are no longer followed by default, and a 3xx response raises an exception with the status as its code instead of being returned as the body; setFollowRedirects(true) restores following. setResolver() takes a callback that receives each request URL and returns the CURLOPT_RESOLVE entries the connection must use, so curl connects to pre-validated addresses instead of resolving the host again. The callback can throw to refuse a URL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
Most Appwrite source requests go through the Appwrite PHP SDK client, not Target::call, so a resolver set on the source left those requests free to resolve the host again and follow redirects anywhere. SDK 30 builds its transport in an overridable createHttpClient(), so the source now uses a Client subclass that never follows redirects, allows only http and https, and passes the resolver's CURLOPT_RESOLVE entries for the endpoint to cURL. A 3xx raises an AppwriteException with the status as its code, matching Target::call; responseType 'location' still returns the Location header. setResolver() on the source forwards to the client. SDK 30 requires utopia-php/client 0.4 and PHP 8.5 syntax, which pulls utopia-php/storage, database and validators forward and needs PHPStan 2 to parse. PHPStan 2 also flagged two stale ignores, a malformed callable docblock and string content-length arithmetic, fixed here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
The worker validated a stored Appwrite endpoint once, but the source then resolved the host again for every request and the SDK client followed redirects, so a host that changed its DNS answer after the check, or redirected, could still reach a private address. Endpoint::resolve() validates a URL and returns the CURLOPT_RESOLVE entries for it, throwing the same invalid-endpoint error as the early check. processSource() sets it as the Appwrite source's resolver unless the endpoint is the worker's own internal one, so every source request is validated and connects to the addresses that were checked. The internal endpoint is now built by one helper. utopia-php/migration points at the 1.14.x backport branch of utopia-php/migration#229 (resolver support and no redirect following for source requests on SDK 26) until 1.14.2 is tagged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ated addresses The endpoint was validated once, and then the Appwrite source resolved the host again for every request it sent and followed redirects, so the checked addresses were not necessarily the ones it connected to. Apply the 2.0.x and 1.9.x change here. Endpoint::resolve() validates a request URL and returns the CURLOPT_RESOLVE entries for the addresses it checked. The report route and the worker pass it to the Appwrite source through setResolver(), so every source request is validated again and connects only to those addresses. The worker skips its own internal _APP_MIGRATION_HOST endpoint, which now comes from one helper shared with processMigration() and authenticateSource(). setResolver() comes from utopia-php/migration#230, the backport cut from 2.0.8, the version main locks today. It overrides the SDK 27 client's call(). utopia-php/migration#229, the main-line change, builds on Appwrite SDK 30, which imports Utopia\Client; this repository's packages/client only provides Utopia\Client\Client, so that change cannot load here yet. The requirement points at the backport branch until 2.0.9 is tagged, and the lock change covers only that package. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main moved Target::call() and the Appwrite SDK onto utopia-php/client 0.6, so the resolver and redirect controls are rebuilt on that transport: Target builds a dedicated client for resolver-pinned requests (with the resolve entries and proxies disabled) and keeps one shared client otherwise, and the source client imports Utopia\Client\Client. The transport now refuses non-HTTP schemes before curl runs, so the protocol test asserts the transport refusal rather than curl's wording. Also brings over the 2.0.x fix that keeps resolver-pinned requests off environment proxies, with its regression tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged
Behaviour to review: The public API used by appwrite/appwrite#14034 is unchanged: Locally on the merged head: Pint passes, PHPStan level 3 passes, and @greptileai review |
…lver utopia-php/migration#229 landed the source resolver and redirect controls on 3.x main, so the source takes CURLOPT_RESOLVE pins from Endpoint again instead of a destinations policy from a separate branch. Require migration main until it is tagged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3.2.0 is the first tag carrying the source request resolver from utopia-php/migration#229, so the dev-main alias is no longer needed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What
Target::call()gains three controls:CURLOPT_PROTOCOLS_STRandCURLOPT_REDIR_PROTOCOLS_STRare set tohttp,https, so an endpoint or redirect with any other scheme (file,gopher,dict, ...) is refused by curl.setFollowRedirects(true)turns following back on.setResolver(?Closure)takes a callback that receives each request URL and returns theCURLOPT_RESOLVEentries (host:port:address[,address]) the connection must use, so curl connects to addresses the consumer has already validated instead of resolving the host again. The callback can throw to refuse a URL.Appwrite source SDK client
Most Appwrite source requests go through the Appwrite PHP SDK client rather than
Target::call(). The source now builds its client fromSources\Appwrite\Client, a subclass of the SDK 30Clientthat overridescreateHttpClient():AppwriteExceptionwith the status as its code, asTarget::call()does (responseType: 'location'still returns theLocationheader);httpandhttpsare allowed;setResolver()takes the same callback asTarget, called with the endpoint URL, and itsCURLOPT_RESOLVEentries apply to every SDK request. The client is rebuilt when the endpoint changes.Sources\Appwrite::setResolver()passes the resolver to bothTarget::call()and the SDK client.Why
Consumers that validate a source endpoint before running a migration need every request to reach the address they approved.
Behaviour change
Redirects are no longer followed unless a consumer opts in, so an endpoint that answers with a redirect (for example
http://tohttps://) now fails with that status instead of being followed. Without a resolver, connections resolve exactly as before.Dependencies
appwrite/appwrite^27.0→^30.0. SDK 29+ needsutopia-php/client0.4, which pullsutopia-php/storage4.2,utopia-php/database7.4 andutopia-php/validators1.0 forward in the lock.utopia-php/client^0.4.1is now required directly, since the subclass builds its cURL adapter.X-Appwrite-Response-Format: 2.0.0.phpstan/phpstan1.*→^2.1, because PHPStan 1 can't parse the SDK's PHP 8.5 syntax and reports every SDK class as unknown. PHPStan 2 also flagged two stale ignores, a malformed callable docblock and stringcontent-lengtharithmetic, all fixed.Tests
tests/Migration/Unit/General/TargetRequestTest.phpruns against a localphp -Sfixture:setFollowRedirects(true);file://is refused;pinned.invalidto127.0.0.1and receives the full request URL;gopher://,dict://andfile://endpoints are refused.tests/Migration/Unit/Sources/AppwriteClientTest.phpuses the same fixture for the SDK client:AppwriteExceptionwith code 302, and only one request reaches the fixture;responseType: 'location'still returns the redirect target;pinned.invalidfor every request, is called once with the endpoint, and runs again aftersetEndpoint();Sources\Appwrite::setResolver()reaches the SDK client:report()sendsGET /v1/usersto the pinned host.With
Target.phpreverted, all 8TargetRequestTestcases fail. Without thecreateHttpClient()override and the forwarding insetResolver(), 5 of the 6AppwriteClientTestcases fail. Thelocationcase guards existing behaviour. The unit suite,pint --testandphpstan --level 3pass locally.🤖 Generated with Claude Code