Merge pull request #762 from stonerl/allow-username-and-password-in-url

Invidious: propper HTTP basic auth support
This commit is contained in:
Arkadiusz Fal 2024-08-31 12:46:33 +02:00 committed by GitHub
commit 9a650b4ac0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 67 additions and 10 deletions

View File

@ -10,11 +10,28 @@ struct AccountsBridge: Defaults.Bridge {
return nil return nil
} }
// Parse the urlString to check for embedded username and password
var sanitizedUrlString = value.urlString
if var urlComponents = URLComponents(string: value.urlString) {
if let user = urlComponents.user, let password = urlComponents.password {
// Sanitize the embedded username and password
let sanitizedUser = user.addingPercentEncoding(withAllowedCharacters: .urlUserAllowed) ?? user
let sanitizedPassword = password.addingPercentEncoding(withAllowedCharacters: .urlPasswordAllowed) ?? password
// Update the URL components with sanitized credentials
urlComponents.user = sanitizedUser
urlComponents.password = sanitizedPassword
// Reconstruct the sanitized URL
sanitizedUrlString = urlComponents.string ?? value.urlString
}
}
return [ return [
"id": value.id, "id": value.id,
"instanceID": value.instanceID ?? "", "instanceID": value.instanceID ?? "",
"name": value.name, "name": value.name,
"apiURL": value.urlString, "apiURL": sanitizedUrlString,
"username": value.username, "username": value.username,
"password": value.password ?? "" "password": value.password ?? ""
] ]

View File

@ -247,27 +247,27 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
} }
func feed(_ page: Int?) -> Resource? { func feed(_ page: Int?) -> Resource? {
resource(baseURL: account.url, path: "\(Self.basePath)/auth/feed") resourceWithAuthCheck(baseURL: account.url, path: "\(Self.basePath)/auth/feed")
.withParam("page", String(page ?? 1)) .withParam("page", String(page ?? 1))
} }
var feed: Resource? { var feed: Resource? {
resource(baseURL: account.url, path: basePathAppending("auth/feed")) resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/feed"))
} }
var subscriptions: Resource? { var subscriptions: Resource? {
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions")) resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
} }
func subscribe(_ channelID: String, onCompletion: @escaping () -> Void = {}) { func subscribe(_ channelID: String, onCompletion: @escaping () -> Void = {}) {
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions")) resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
.child(channelID) .child(channelID)
.request(.post) .request(.post)
.onCompletion { _ in onCompletion() } .onCompletion { _ in onCompletion() }
} }
func unsubscribe(_ channelID: String, onCompletion: @escaping () -> Void) { func unsubscribe(_ channelID: String, onCompletion: @escaping () -> Void) {
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions")) resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
.child(channelID) .child(channelID)
.request(.delete) .request(.delete)
.onCompletion { _ in onCompletion() } .onCompletion { _ in onCompletion() }
@ -308,11 +308,11 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
return nil return nil
} }
return resource(baseURL: account.url, path: basePathAppending("auth/playlists")) return resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/playlists"))
} }
func playlist(_ id: String) -> Resource? { func playlist(_ id: String) -> Resource? {
resource(baseURL: account.url, path: basePathAppending("auth/playlists/\(id)")) resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/playlists/\(id)"))
} }
func playlistVideos(_ id: String) -> Resource? { func playlistVideos(_ id: String) -> Resource? {
@ -445,6 +445,9 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
urlComponents.scheme = instanceURLComponents.scheme urlComponents.scheme = instanceURLComponents.scheme
urlComponents.host = instanceURLComponents.host urlComponents.host = instanceURLComponents.host
urlComponents.user = instanceURLComponents.user
urlComponents.password = instanceURLComponents.password
urlComponents.port = instanceURLComponents.port
guard let url = urlComponents.url else { guard let url = urlComponents.url else {
return nil return nil
@ -553,6 +556,30 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
) )
} }
// Determines if the request requires Basic Auth credentials to be removed
private func needsBasicAuthRemoval(for path: String) -> Bool {
return path.hasPrefix("\(Self.basePath)/auth/")
}
// Creates a resource URL with consideration for removing Basic Auth credentials
private func createResourceURL(baseURL: URL, path: String) -> URL {
var resourceURL = baseURL
// Remove Basic Auth credentials if required
if needsBasicAuthRemoval(for: path), var urlComponents = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) {
urlComponents.user = nil
urlComponents.password = nil
resourceURL = urlComponents.url ?? baseURL
}
return resourceURL.appendingPathComponent(path)
}
func resourceWithAuthCheck(baseURL: URL, path: String) -> Resource {
let sanitizedURL = createResourceURL(baseURL: baseURL, path: path)
return super.resource(absoluteURL: sanitizedURL)
}
private func extractThumbnails(from details: JSON) -> [Thumbnail] { private func extractThumbnails(from details: JSON) -> [Thumbnail] {
details["videoThumbnails"].arrayValue.compactMap { json in details["videoThumbnails"].arrayValue.compactMap { json in
guard let url = json["url"].url, guard let url = json["url"].url,
@ -563,13 +590,20 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
return nil return nil
} }
// some of instances are not configured properly and return thumbnails links // Some instances are not configured properly and return thumbnail links
// with incorrect scheme // with an incorrect scheme or a missing port.
components.scheme = accountUrlComponents.scheme components.scheme = accountUrlComponents.scheme
components.port = accountUrlComponents.port
// If basic HTTP authentication is used,
// the username and password need to be prepended to the URL.
components.user = accountUrlComponents.user
components.password = accountUrlComponents.password
guard let thumbnailUrl = components.url else { guard let thumbnailUrl = components.url else {
return nil return nil
} }
print("Final thumbnail URL: \(thumbnailUrl)")
return Thumbnail(url: thumbnailUrl, quality: .init(rawValue: quality)!) return Thumbnail(url: thumbnailUrl, quality: .init(rawValue: quality)!)
} }

View File

@ -86,6 +86,7 @@ struct InstanceForm: View {
.autocapitalization(.none) .autocapitalization(.none)
.keyboardType(.URL) .keyboardType(.URL)
#endif #endif
.disableAutocorrection(true)
#if os(tvOS) #if os(tvOS)
VStack { VStack {

View File

@ -66,6 +66,11 @@
value = "Yes" value = "Yes"
isEnabled = "YES"> isEnabled = "YES">
</EnvironmentVariable> </EnvironmentVariable>
<EnvironmentVariable
key = "IDELogRedirectionPolicy"
value = "oslogToStdio"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables> </EnvironmentVariables>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction