mirror of
https://github.com/yattee/yattee.git
synced 2026-05-12 10:25:02 +00:00
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:
@@ -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" : {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -129,35 +129,37 @@ private struct EditRemoteServerContent: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
Section {
|
||||
#if os(tvOS)
|
||||
TVSettingsTextField(title: String(localized: "sources.field.username"), text: $basicAuthUsername)
|
||||
TVSettingsTextField(title: String(localized: "sources.field.password"), text: $basicAuthPassword, isSecure: true)
|
||||
#elseif os(macOS)
|
||||
LabeledContent(String(localized: "sources.field.username")) {
|
||||
TextField("", text: $basicAuthUsername)
|
||||
if instance.supportsHTTPBasicAuthProxy {
|
||||
Section {
|
||||
#if os(tvOS)
|
||||
TVSettingsTextField(title: String(localized: "sources.field.username"), text: $basicAuthUsername)
|
||||
TVSettingsTextField(title: String(localized: "sources.field.password"), text: $basicAuthPassword, isSecure: true)
|
||||
#elseif os(macOS)
|
||||
LabeledContent(String(localized: "sources.field.username")) {
|
||||
TextField("", text: $basicAuthUsername)
|
||||
.textContentType(.username)
|
||||
.autocorrectionDisabled()
|
||||
}
|
||||
LabeledContent(String(localized: "sources.field.password")) {
|
||||
SecureField("", text: $basicAuthPassword)
|
||||
.textContentType(.password)
|
||||
}
|
||||
#else
|
||||
TextField(String(localized: "sources.field.username"), text: $basicAuthUsername)
|
||||
.textContentType(.username)
|
||||
#if os(iOS)
|
||||
.textInputAutocapitalization(.never)
|
||||
#endif
|
||||
.autocorrectionDisabled()
|
||||
}
|
||||
LabeledContent(String(localized: "sources.field.password")) {
|
||||
SecureField("", text: $basicAuthPassword)
|
||||
.textContentType(.password)
|
||||
}
|
||||
#else
|
||||
TextField(String(localized: "sources.field.username"), text: $basicAuthUsername)
|
||||
.textContentType(.username)
|
||||
#if os(iOS)
|
||||
.textInputAutocapitalization(.never)
|
||||
#endif
|
||||
.autocorrectionDisabled()
|
||||
|
||||
SecureField(String(localized: "sources.field.password"), text: $basicAuthPassword)
|
||||
.textContentType(.password)
|
||||
#endif
|
||||
} header: {
|
||||
Text(String(localized: instance.type == .yatteeServer ? "sources.header.auth" : "sources.header.basicAuth"))
|
||||
} footer: {
|
||||
Text(String(localized: instance.type == .yatteeServer ? "sources.footer.yatteeServerAuth" : "sources.footer.basicAuth"))
|
||||
SecureField(String(localized: "sources.field.password"), text: $basicAuthPassword)
|
||||
.textContentType(.password)
|
||||
#endif
|
||||
} header: {
|
||||
Text(String(localized: instance.type == .yatteeServer ? "sources.header.auth" : "sources.header.basicAuth"))
|
||||
} footer: {
|
||||
Text(String(localized: instance.type == .yatteeServer ? "sources.footer.yatteeServerAuth" : "sources.footer.basicAuth"))
|
||||
}
|
||||
}
|
||||
|
||||
if instance.type == .yatteeServer {
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user