Block HTTP Basic Auth proxy for Piped sources

Piped's session token reuses the Authorization header, so a fronting basic
auth proxy can't coexist with logged-in Piped use — the two would clobber
each other's credentials on every authenticated request.

Add a supportsHTTPBasicAuthProxy capability on Instance/InstanceType (false
for Piped, true for everything else) and route it through:

- AddRemoteServerView refuses Piped if detection only succeeded behind basic
  auth, surfacing a localized "not supported" error instead of a silently
  broken instance, and hides the optional credentials section for Piped.
- EditSourceView hides the basic auth fields for Piped instances and clears
  any legacy stored credentials on save, in case a Piped source was added
  with credentials before this change.
This commit is contained in:
Arkadiusz Fal
2026-05-06 20:17:18 +02:00
parent 11841d7b41
commit 6f8aa9a1b3
4 changed files with 80 additions and 37 deletions

View File

@@ -15083,6 +15083,17 @@
}
}
},
"sources.error.pipedBasicAuthUnsupported" : {
"comment" : "Error when a Piped instance is detected behind an HTTP Basic Auth proxy. Piped uses the same Authorization header for the user session, so the two cannot coexist.",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Piped sources can't be used behind an HTTP Basic Auth proxy. Piped reuses the Authorization header for the user session, so it conflicts with the proxy's credentials."
}
}
}
},
"sources.error.couldNotDetect" : {
"comment" : "Error when source type could not be detected",
"localizations" : {

View File

@@ -182,6 +182,22 @@ extension Instance {
var supportsVideoProxying: Bool {
type == .invidious || type == .piped || type == .yatteeServer
}
/// Whether this instance can sit behind an HTTP Basic Auth reverse proxy.
/// Piped is excluded: its session token is sent in the same `Authorization`
/// header that the proxy would consume, so logged-in features can't coexist
/// with proxy credentials.
var supportsHTTPBasicAuthProxy: Bool {
type.supportsHTTPBasicAuthProxy
}
}
extension InstanceType {
/// See `Instance.supportsHTTPBasicAuthProxy`. Type-only variant for flows
/// (e.g., AddRemoteServer detection) that don't yet have an `Instance`.
var supportsHTTPBasicAuthProxy: Bool {
self != .piped
}
}
// MARK: - Instance Validation

View File

@@ -439,11 +439,13 @@ struct AddRemoteServerView: View {
}
// HTTP Basic Auth credentials.
// Required for Yattee Server (always shown). Optional for other types show only
// when credentials were already provided (e.g., via the basic-auth-required retry
// path), so we don't clutter the form for the normal "no proxy" case.
let showBasicAuthSection = detectedType == .yatteeServer
|| (!basicAuthUsername.isEmpty || !basicAuthPassword.isEmpty)
// Required for Yattee Server (always shown). Optional for types that can sit
// behind a basic-auth proxy show only when credentials were already provided
// (e.g., via the basic-auth-required retry path), so we don't clutter the form
// for the normal "no proxy" case.
let showBasicAuthSection = (detectedType?.supportsHTTPBasicAuthProxy ?? false) &&
(detectedType == .yatteeServer
|| (!basicAuthUsername.isEmpty || !basicAuthPassword.isEmpty))
if showBasicAuthSection {
Section {
@@ -574,6 +576,14 @@ struct AddRemoteServerView: View {
switch result {
case .success(let detectionResult):
LoggingService.shared.debug("[AddRemoteServerView] Detection succeeded: \(detectionResult.type)", category: .api)
if basicAuthHeader != nil, !detectionResult.type.supportsHTTPBasicAuthProxy {
withAnimation {
self.uiState = .error(String(localized: "sources.error.pipedBasicAuthUnsupported"))
}
return
}
withAnimation {
self.detectedType = detectionResult.type
self.detectionResult = detectionResult
@@ -764,7 +774,7 @@ struct AddRemoteServerView: View {
allowInvalidCertificates: allowInvalidCertificates
)
if !basicAuthUsername.isEmpty && !basicAuthPassword.isEmpty {
if !basicAuthUsername.isEmpty, !basicAuthPassword.isEmpty {
appEnvironment.basicAuthCredentialsManager.setCredentials(
username: basicAuthUsername,
password: basicAuthPassword,

View File

@@ -129,6 +129,7 @@ private struct EditRemoteServerContent: View {
#endif
}
if instance.supportsHTTPBasicAuthProxy {
Section {
#if os(tvOS)
TVSettingsTextField(title: String(localized: "sources.field.username"), text: $basicAuthUsername)
@@ -159,6 +160,7 @@ private struct EditRemoteServerContent: View {
} footer: {
Text(String(localized: instance.type == .yatteeServer ? "sources.footer.yatteeServerAuth" : "sources.footer.basicAuth"))
}
}
if instance.type == .yatteeServer {
// Server Info Section
@@ -452,16 +454,20 @@ private struct EditRemoteServerContent: View {
updated.allowInvalidCertificates = allowInvalidCertificates
updated.proxiesVideos = proxiesVideos
// Save / clear HTTP Basic Auth credentials.
// Works for any instance type, but for Yattee Server we never auto-clear
// (credentials are required there; the user can re-enter to overwrite).
if !basicAuthUsername.isEmpty && !basicAuthPassword.isEmpty {
// Save / clear HTTP Basic Auth credentials. Yattee Server never auto-clears
// (credentials are required and the user re-enters to overwrite). Piped never
// persists them its session token reuses the same Authorization header.
if !instance.supportsHTTPBasicAuthProxy {
if hadStoredBasicAuth {
appEnvironment?.basicAuthCredentialsManager.deleteCredentials(for: instance)
}
} else if !basicAuthUsername.isEmpty, !basicAuthPassword.isEmpty {
appEnvironment?.basicAuthCredentialsManager.setCredentials(
username: basicAuthUsername,
password: basicAuthPassword,
for: instance
)
} else if hadStoredBasicAuth && instance.type != .yatteeServer {
} else if hadStoredBasicAuth, instance.type != .yatteeServer {
appEnvironment?.basicAuthCredentialsManager.deleteCredentials(for: instance)
}