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
35 changes: 35 additions & 0 deletions Sources/CodexBarCore/Providers/Amp/AmpUsageFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,9 @@ public struct AmpUsageFetcher: Sendable {
if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 {
throw AmpUsageError.invalidCredentials
}
if diagnostics.detectedLoginRedirect {
throw AmpUsageError.invalidCredentials
}
throw AmpUsageError.networkError("HTTP \(httpResponse.statusCode)")
}

Expand All @@ -277,6 +280,7 @@ public struct AmpUsageFetcher: Sendable {
private let cookieHeader: String
private let logger: ((String) -> Void)?
var redirects: [String] = []
private(set) var detectedLoginRedirect = false

init(cookieHeader: String, logger: ((String) -> Void)?) {
self.cookieHeader = cookieHeader
Expand All @@ -293,6 +297,16 @@ public struct AmpUsageFetcher: Sendable {
let from = response.url?.absoluteString ?? "unknown"
let to = request.url?.absoluteString ?? "unknown"
self.redirects.append("\(response.statusCode) \(from) -> \(to)")

if let toURL = request.url, AmpUsageFetcher.isLoginRedirect(toURL) {
if let logger {
logger("[amp] Detected login redirect, aborting (invalid session)")
}
self.detectedLoginRedirect = true
completionHandler(nil)
return
}

var updated = request
if AmpUsageFetcher.shouldAttachCookie(to: request.url), !self.cookieHeader.isEmpty {
updated.setValue(self.cookieHeader, forHTTPHeaderField: "Cookie")
Expand Down Expand Up @@ -364,4 +378,25 @@ public struct AmpUsageFetcher: Sendable {
if host == "ampcode.com" || host == "www.ampcode.com" { return true }
return host.hasSuffix(".ampcode.com")
}

static func isLoginRedirect(_ url: URL) -> Bool {
guard self.shouldAttachCookie(to: url) else { return false }

let path = url.path.lowercased()
let components = path.split(separator: "/").map(String.init)
if components.contains("login") { return true }
if components.contains("signin") { return true }
if components.contains("sign-in") { return true }

// Amp currently redirects to /auth/sign-in?returnTo=... when session is invalid. Keep this slightly broader
// than one exact path so we keep working if Amp changes auth routes.
if components.contains("auth") {
let query = url.query?.lowercased() ?? ""
if query.contains("returnto=") { return true }
if query.contains("redirect=") { return true }
if query.contains("redirectto=") { return true }
}

return false
}
}
27 changes: 27 additions & 0 deletions Tests/CodexBarTests/AmpUsageFetcherTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,31 @@ struct AmpUsageFetcherTests {
#expect(!AmpUsageFetcher.shouldAttachCookie(to: URL(string: "https://ampcode.com.evil.com")))
#expect(!AmpUsageFetcher.shouldAttachCookie(to: nil))
}

@Test
func detectsLoginRedirects() throws {
let signIn = try #require(URL(string: "https://ampcode.com/auth/sign-in?returnTo=%2Fsettings"))
#expect(AmpUsageFetcher.isLoginRedirect(signIn))

let sso = try #require(URL(string: "https://ampcode.com/auth/sso?returnTo=%2Fsettings"))
#expect(AmpUsageFetcher.isLoginRedirect(sso))

let login = try #require(URL(string: "https://ampcode.com/login"))
#expect(AmpUsageFetcher.isLoginRedirect(login))

let signin = try #require(URL(string: "https://www.ampcode.com/signin"))
#expect(AmpUsageFetcher.isLoginRedirect(signin))
}

@Test
func ignoresNonLoginURLs() throws {
let settings = try #require(URL(string: "https://ampcode.com/settings"))
#expect(!AmpUsageFetcher.isLoginRedirect(settings))

let signOut = try #require(URL(string: "https://ampcode.com/auth/sign-out"))
#expect(!AmpUsageFetcher.isLoginRedirect(signOut))

let evil = try #require(URL(string: "https://ampcode.com.evil.com/auth/sign-in"))
#expect(!AmpUsageFetcher.isLoginRedirect(evil))
}
}