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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
- CLI: add `codexbar serve` for localhost JSON access to usage and cost endpoints (#957). Thanks @ThiagoCAltoe!

### Fixed
- OpenCode Go: block cross-host redirects when fetching usage so imported cookies cannot follow external redirect targets (#969). Thanks @pavbar!
- Codex: keep background `/status` probes out of Codex Desktop history by using isolated non-persistent CLI storage (#953).
- Menu: stabilize the Cost submenu by using a native menu item and deferring open-menu rebuilds while tracking (#954). Thanks @getogrand!
- Localization: add Brazilian Portuguese quota-warning settings strings (#958). Thanks @ThiagoCAltoe!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,25 @@ public struct OpenCodeGoUsageFetcher: Sendable {
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " +
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"

private final class RedirectGuardDelegate: NSObject, URLSessionTaskDelegate {
func urlSession(
_ session: URLSession,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest,
completionHandler: @escaping (URLRequest?) -> Void)
{
guard OpenCodeGoUsageFetcher.allowsRedirect(
from: task.originalRequest?.url,
to: request.url)
else {
completionHandler(nil)
return
}
completionHandler(request)
}
}

private struct ServerRequest {
let serverID: String
let args: String?
Expand Down Expand Up @@ -73,14 +92,24 @@ public struct OpenCodeGoUsageFetcher: Sendable {
"renewAt",
"renew_at",
]
private static let redirectGuardDelegate = RedirectGuardDelegate()
private static let redirectGuardSession: URLSession = {
let configuration = URLSessionConfiguration.ephemeral
configuration.httpCookieStorage = nil
return URLSession(
configuration: configuration,
delegate: OpenCodeGoUsageFetcher.redirectGuardDelegate,
delegateQueue: nil)
}()

public static func fetchUsage(
cookieHeader: String,
timeout: TimeInterval,
now: Date = Date(),
workspaceIDOverride: String? = nil,
session: URLSession = .shared) async throws -> OpenCodeGoUsageSnapshot
session: URLSession? = nil) async throws -> OpenCodeGoUsageSnapshot
{
let session = session ?? self.redirectGuardSession
guard let requestCookieHeader = OpenCodeWebCookieSupport.requestCookieHeader(from: cookieHeader) else {
throw OpenCodeGoUsageError.invalidCredentials
}
Expand All @@ -100,6 +129,15 @@ public struct OpenCodeGoUsageFetcher: Sendable {
return try self.parseSubscription(text: subscriptionText, now: now)
}

static func allowsRedirect(from sourceURL: URL?, to destinationURL: URL?) -> Bool {
guard let sourceHost = sourceURL?.host?.lowercased(),
let destinationHost = destinationURL?.host?.lowercased(),
sourceHost == destinationHost,
destinationURL?.scheme?.lowercased() == "https"
else { return false }
return true
}

public static func dashboardURL(workspaceID raw: String?) -> URL {
guard let workspaceID = self.normalizeWorkspaceID(raw),
let url = URL(string: "\(self.baseURL.absoluteString)/workspace/\(workspaceID)/go")
Expand Down
17 changes: 16 additions & 1 deletion Tests/CodexBarTests/OpenCodeGoUsageFetcherErrorTests.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import CodexBarCore
import Foundation
import Testing
@testable import CodexBarCore

@Suite(.serialized)
struct OpenCodeGoUsageFetcherErrorTests {
Expand Down Expand Up @@ -28,6 +28,21 @@ struct OpenCodeGoUsageFetcherErrorTests {
return URLSession(configuration: config)
}

@Test
func `redirect guard allows only same-host https redirects`() {
#expect(OpenCodeGoUsageFetcher.allowsRedirect(
from: URL(string: "https://opencode.ai/_server"),
to: URL(string: "https://opencode.ai/workspace/wrk_TEST123/go")))

#expect(!OpenCodeGoUsageFetcher.allowsRedirect(
from: URL(string: "https://opencode.ai/_server"),
to: URL(string: "https://evil.example/steal")))

#expect(!OpenCodeGoUsageFetcher.allowsRedirect(
from: URL(string: "https://opencode.ai/_server"),
to: URL(string: "http://opencode.ai/insecure")))
}

@Test
func `extracts api error from detail field`() async throws {
defer {
Expand Down