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
7 changes: 7 additions & 0 deletions .changeset/required-resolver-fetchers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@germ-network/atprotoclient": minor
---

Remove the `URLSession` defaults so non-Apple platforms can depend on the fetcher-based API without reaching `URLSession`.

Breaking: `DidWebResolver.init(fetcher:)`, `DidPlcResolver.init(directory:fetcher:)` and `WellKnownHandleResolver.init(fetcher:)` no longer default to a `URLSession.manualRedirect()`-backed fetcher, and now take an `any RedirectRefusingHTTPFetcher` (from GermConvenience 0.14.0, now required), so a redirect-following fetcher no longer compiles. Pass `ManualRedirectFetcher()` from `GermConvenienceURLSession`, or any conformer of your own. `DnsHandleResolver.init(txtFetcher:)` no longer defaults either; pass a `DNSTXTFetcher`, e.g. `DoHTXTFetcher(fetcher:)`. `StubHTTPFetcher` now conforms to `RedirectRefusingHTTPFetcher`. Also drops the unused `FoundationNetworking` imports from the four resolver files.
18 changes: 18 additions & 0 deletions .github/workflows/ci-android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,24 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: "Test Swift Package on Android"
id: android
uses: skiptools/swift-android-action@v2
with:
swift-version: '6.3'
# The portable products must not link FoundationNetworking. Built into a
# scratch directory of their own so the objects scanned are exactly their
# closure. `--target`, not `--product`: SwiftPM 6.3 rejects `--product`
# for a library product and silently builds every target instead.
- name: "Portable products do not link FoundationNetworking"
run: |
for target in AtprotoClient AtprotoClientMocks; do
${{ steps.android.outputs.swift-build }} --scratch-path .build-portable --target "$target"
done
Scripts/scan-foundation-networking.sh .build-portable
# Control: the scanner must flag a target that does use URLSession (the
# test target), or a pass above could just mean it is not seeing the
# autolink entry.
- name: "Scanner detects FoundationNetworking in the test target"
run: |
${{ steps.android.outputs.swift-build }} --scratch-path .build-control --target AtprotoClientTests
Scripts/scan-foundation-networking.sh --expect .build-control
6 changes: 3 additions & 3 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ let package = Package(
),
.package(
url: "https://github.com/germ-network/GermConvenience.git",
// 0.11.0: manualRedirect() also refuses redirects on Linux/Android, which the
// DID/handle resolvers' one-time host screening depends on.
from: "0.11.0"
// 0.14.0: RedirectRefusingHTTPFetcher, required by the resolvers that
// screen a host once before fetching. 0.13.0 moved the URLSession
// conformers into GermConvenienceURLSession, so the portable targets
// here no longer reach URLSession.
from: "0.14.0"
),
.package(
url: "https://github.com/apple/swift-crypto.git",
Expand Down Expand Up @@ -64,6 +66,7 @@ let package = Package(
dependencies: [
"AtprotoClient", "AtprotoClientMocks",
.product(name: "GermConvenienceHTTP", package: "GermConvenience"),
.product(name: "GermConvenienceURLSession", package: "GermConvenience"),
]
),
]
Expand Down
48 changes: 48 additions & 0 deletions Scripts/scan-foundation-networking.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# Scans the object files of a SwiftPM scratch directory for the
# `-lFoundationNetworking` autolink entry Swift records for any module that
# imports FoundationNetworking, and exits 1 if one is found (or 0 if --expect
# is given and one is). A module that carries the entry puts
# libFoundationNetworking.so in the NEEDED list of whatever links it.
#
# usage: scan-foundation-networking.sh [--expect] <scratch-path>
set -euo pipefail

expect=false
if [[ "${1:-}" == "--expect" ]]; then
expect=true
shift
fi
scratch="${1:?usage: $0 [--expect] <scratch-path>}"

objects=()
while IFS= read -r -d '' object; do
objects+=("$object")
done < <(find "$scratch" -name '*.o' -path '*android*' -print0)

if [[ ${#objects[@]} -eq 0 ]]; then
echo "error: no Android object files under $scratch; nothing was scanned" >&2
exit 2
fi

hits=()
for object in "${objects[@]}"; do
if strings "$object" | grep -- '-lFoundationNetworking' >/dev/null; then
hits+=("$object")
fi
done

if $expect; then
if [[ ${#hits[@]} -eq 0 ]]; then
echo "error: expected a FoundationNetworking autolink entry but found none (scanner is not detecting it)" >&2
exit 1
fi
echo "ok: scanner detects FoundationNetworking in ${#hits[@]} of ${#objects[@]} objects"
else
if [[ ${#hits[@]} -gt 0 ]]; then
echo "error: FoundationNetworking is linked by:" >&2
printf ' %s\n' "${hits[@]}" >&2
exit 1
fi
echo "ok: none of ${#objects[@]} objects link FoundationNetworking"
fi
16 changes: 5 additions & 11 deletions Sources/AtprotoClient/DNS/DnsHandleResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ import Foundation
import GermConvenience
import GermConvenienceHTTP

#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

extension Atproto {
/// Resolves an atproto handle via its `_atproto` DNS TXT record, as
/// specified at https://atproto.com/specs/handle#dns-txt-method.
Expand All @@ -24,13 +20,11 @@ extension Atproto {

let txtFetcher: any DNSTXTFetcher

/// Defaults to `DoHTXTFetcher`, the only conformer this package ships -
/// portable everywhere, unlike a platform system resolver. Inject a
/// platform-specific `DNSTXTFetcher` to use one instead.
public init(
txtFetcher: any DNSTXTFetcher = DoHTXTFetcher(
fetcher: URLSession.manualRedirect())
) {
/// `DoHTXTFetcher`, over a redirect-refusing `HTTPFetcher`, is the
/// conformer this package ships - portable everywhere, unlike a
/// platform system resolver. Pass a platform-specific `DNSTXTFetcher`
/// to use one instead.
public init(txtFetcher: any DNSTXTFetcher) {
self.txtFetcher = txtFetcher
}

Expand Down
14 changes: 5 additions & 9 deletions Sources/AtprotoClient/DNS/WellKnownHandleResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ import GermConvenience
import GermConvenienceHTTP
import HTTPTypes

#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

extension Atproto {
/// Resolves an atproto handle by fetching
/// `https://<handle>/.well-known/atproto-did`, as specified at
Expand All @@ -33,12 +29,12 @@ extension Atproto {
/// buffered beforehand.
static let maxBodySize = 8192

let fetcher: any HTTPFetcher
let fetcher: any RedirectRefusingHTTPFetcher

/// Defaults to a redirect-refusing session, for the same reason
/// `DidWebResolver` refuses one - the handle is already validated, but
/// a followed redirect would fetch a second, unvalidated host.
public init(fetcher: any HTTPFetcher = URLSession.manualRedirect()) {
/// `fetcher` must refuse redirects, which the type requires, for the same
/// reason `DidWebResolver` does - the handle is already validated, but a
/// followed redirect would fetch a second, unvalidated host.
public init(fetcher: any RedirectRefusingHTTPFetcher) {
self.fetcher = fetcher
}

Expand Down
14 changes: 5 additions & 9 deletions Sources/AtprotoClient/DidPlcResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ import GermConvenience
import GermConvenienceHTTP
import HTTPTypes

#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

extension Atproto {
/// Resolves a did:plc identifier by fetching `https://plc.directory/<did>`.
///
Expand All @@ -23,18 +19,18 @@ extension Atproto {
static let acceptHeader = "application/did+ld+json,application/json"

let directory: URL
let fetcher: any HTTPFetcher
let fetcher: any RedirectRefusingHTTPFetcher

/// `directory` must be a bare `https` origin — no path, query,
/// fragment, or userinfo. Anything else (a trailing slash, most
/// easily) silently breaks URL construction and turns every
/// resolution into a `nil`, so it's rejected here instead. `fetcher`
/// defaults to a redirect-refusing session, for the same reason
/// `DidWebResolver` refuses one — a followed redirect would bypass
/// identifier validation, which only runs once, before the request.
/// must refuse redirects, which the type requires, for the same reason
/// `DidWebResolver` does — a followed redirect would bypass identifier
/// validation, which only runs once, before the request.
public init(
directory: URL = Self.defaultDirectory,
fetcher: any HTTPFetcher = URLSession.manualRedirect()
fetcher: any RedirectRefusingHTTPFetcher
) throws {
guard
let components = URLComponents(
Expand Down
15 changes: 5 additions & 10 deletions Sources/AtprotoClient/DidWebResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ import GermConvenience
import GermConvenienceHTTP
import HTTPTypes

#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

extension Atproto {
/// Resolves a did:web identifier by fetching
/// `https://{host}/.well-known/did.json`.
Expand All @@ -26,13 +22,12 @@ extension Atproto {
static let wellKnownPath = "/.well-known/did.json"
static let acceptHeader = "application/did+ld+json,application/json"

let fetcher: any HTTPFetcher
let fetcher: any RedirectRefusingHTTPFetcher

/// Defaults to a redirect-refusing session — do not override with a
/// redirect-following fetcher in production. Host screening runs once,
/// before the request; a followed redirect bypasses it, and nothing
/// downstream can detect that after the fact.
public init(fetcher: any HTTPFetcher = URLSession.manualRedirect()) {
/// `fetcher` must refuse redirects, which the type requires. Host
/// screening runs once, before the request; a followed redirect bypasses
/// it, and nothing downstream can detect that after the fact.
public init(fetcher: any RedirectRefusingHTTPFetcher) {
self.fetcher = fetcher
}

Expand Down
5 changes: 3 additions & 2 deletions Sources/AtprotoClientMocks/StubHTTPFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import Foundation
import GermConvenience
import GermConvenienceHTTP

/// A canned-response `HTTPFetcher`, for anything built on the seam.
public struct StubHTTPFetcher: HTTPFetcher, Sendable {
/// A canned-response `HTTPFetcher`, for anything built on the seam. It only
/// returns what it's given, so it never follows a redirect.
public struct StubHTTPFetcher: RedirectRefusingHTTPFetcher, Sendable {
private let handler: @Sendable (BundledHTTPRequest) throws -> HTTPDataResponse

public init(
Expand Down
1 change: 1 addition & 0 deletions Tests/AtprotoClientTests/OnlineTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import AtprotoClient
import AtprotoTypes
import Foundation
import GermConvenienceURLSession
import Testing

#if canImport(FoundationNetworking)
Expand Down
Loading