mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-15 12:41:37 +03:00
fix(server): align connect route checks (#55177)
This commit is contained in:
committed by
GitHub
Unverified
parent
a00475a4dd
commit
d8e31d846a
@@ -9,10 +9,12 @@ import { PTY_CONNECT_TICKET_QUERY, PTY_CONNECT_TOKEN_HEADER, PTY_CONNECT_TOKEN_H
|
||||
|
||||
export { PTY_CONNECT_TICKET_QUERY, PTY_CONNECT_TOKEN_HEADER, PTY_CONNECT_TOKEN_HEADER_VALUE }
|
||||
|
||||
const CONNECT_PATH = /^\/api\/experimental\/persistent-pty\/[^/]+\/connect$/
|
||||
export const PERSISTENT_PTY_CONNECT_PATH = "/api/experimental/persistent-pty/:ptyID/connect"
|
||||
|
||||
const PERSISTENT_PTY_CONNECT_URL = /^\/api\/experimental\/persistent-pty\/[^/]+\/connect$/
|
||||
|
||||
export function hasPersistentPtyConnectTicketURL(url: URL) {
|
||||
return CONNECT_PATH.test(url.pathname) && !!url.searchParams.get(PTY_CONNECT_TICKET_QUERY)
|
||||
return PERSISTENT_PTY_CONNECT_URL.test(url.pathname) && !!url.searchParams.get(PTY_CONNECT_TICKET_QUERY)
|
||||
}
|
||||
|
||||
const errors = [InvalidRequestError, ServiceUnavailableError] as const
|
||||
@@ -100,7 +102,7 @@ export const PersistentPtyGroup = HttpApiGroup.make("server.experimental")
|
||||
}),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("persistentPty.connect", "/api/experimental/persistent-pty/:ptyID/connect", {
|
||||
HttpApiEndpoint.get("persistentPty.connect", PERSISTENT_PTY_CONNECT_PATH, {
|
||||
params: { ptyID: Pty.ID },
|
||||
success: Schema.Boolean,
|
||||
error: [ForbiddenError, PtyNotFoundError, ServiceUnavailableError],
|
||||
|
||||
@@ -9,13 +9,12 @@ import { LocationQuery, locationQueryOpenApi } from "./location.js"
|
||||
export const PTY_CONNECT_TICKET_QUERY = "ticket"
|
||||
export const PTY_CONNECT_TOKEN_HEADER = "x-opencode-ticket"
|
||||
export const PTY_CONNECT_TOKEN_HEADER_VALUE = "1"
|
||||
export const PTY_CONNECT_PATH = "/api/pty/:ptyID/connect"
|
||||
|
||||
const PTY_CONNECT_PATH = /^\/api\/pty\/[^/]+\/connect$/
|
||||
const PTY_CONNECT_URL = /^\/api\/pty\/[^/]+\/connect$/
|
||||
|
||||
// Authorization middleware skips credential checks when this matches; the PTY connect handler
|
||||
// is then responsible for consuming and validating the ticket.
|
||||
export function hasPtyConnectTicketURL(url: URL) {
|
||||
return PTY_CONNECT_PATH.test(url.pathname) && !!url.searchParams.get(PTY_CONNECT_TICKET_QUERY)
|
||||
return PTY_CONNECT_URL.test(url.pathname) && !!url.searchParams.get(PTY_CONNECT_TICKET_QUERY)
|
||||
}
|
||||
|
||||
export const makePtyGroup = <LocationId extends HttpApiMiddleware.AnyId, LocationService>(
|
||||
@@ -120,7 +119,7 @@ export const makePtyGroup = <LocationId extends HttpApiMiddleware.AnyId, Locatio
|
||||
.middleware(locationMiddleware)
|
||||
.add(
|
||||
// Query fields are decoded in the raw handler before upgrade work.
|
||||
HttpApiEndpoint.get("pty.connect", "/api/pty/:ptyID/connect", {
|
||||
HttpApiEndpoint.get("pty.connect", PTY_CONNECT_PATH, {
|
||||
params: { ptyID: Pty.ID },
|
||||
success: Schema.Boolean,
|
||||
error: [ForbiddenError, PtyNotFoundError, LocationNotFoundError],
|
||||
|
||||
@@ -30,11 +30,12 @@ export const PairingSession = Schema.Struct({
|
||||
}).annotate({ identifier: "PairingSession" })
|
||||
export type PairingSession = typeof PairingSession.Type
|
||||
|
||||
const PAIRING_CONNECT_PATH = /^\/auth\/connect\/[^/]+$/
|
||||
export const PAIRING_CONNECT_PATH = "/auth/connect/:code"
|
||||
|
||||
const PAIRING_CONNECT_URL = /^\/auth\/connect\/[^/]+$/
|
||||
|
||||
// Authorization middleware skips credential checks for pairing links; the connect handler consumes the code instead.
|
||||
export function isPairingConnectURL(url: URL) {
|
||||
return PAIRING_CONNECT_PATH.test(url.pathname)
|
||||
return PAIRING_CONNECT_URL.test(url.pathname)
|
||||
}
|
||||
|
||||
export const ServerGroup = HttpApiGroup.make("server.server")
|
||||
@@ -61,7 +62,7 @@ export const ServerGroup = HttpApiGroup.make("server.server")
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("server.connect", "/auth/connect/:code", {
|
||||
HttpApiEndpoint.get("server.connect", PAIRING_CONNECT_PATH, {
|
||||
params: { code: Schema.String },
|
||||
success: PairingSession,
|
||||
error: UnauthorizedError,
|
||||
|
||||
@@ -2,12 +2,12 @@ import { ServerAuth } from "../auth"
|
||||
import { UnauthorizedError } from "@opencode/protocol/errors"
|
||||
import { Authorization } from "@opencode/protocol/middleware/authorization"
|
||||
export { Authorization } from "@opencode/protocol/middleware/authorization"
|
||||
import { hasPtyConnectTicketURL } from "@opencode/protocol/groups/pty"
|
||||
import { hasPersistentPtyConnectTicketURL } from "@opencode/protocol/groups/persistent-pty"
|
||||
import { isPairingConnectURL } from "@opencode/protocol/groups/server"
|
||||
import { PTY_CONNECT_PATH, PTY_CONNECT_TICKET_QUERY } from "@opencode/protocol/groups/pty"
|
||||
import { PERSISTENT_PTY_CONNECT_PATH } from "@opencode/protocol/groups/persistent-pty"
|
||||
import { PAIRING_CONNECT_PATH } from "@opencode/protocol/groups/server"
|
||||
import { Effect, Layer, Redacted } from "effect"
|
||||
import { Base64 } from "effect/encoding"
|
||||
import { HttpEffect, HttpServerRequest, HttpServerResponse } from "effect/http"
|
||||
import { HttpEffect, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http"
|
||||
|
||||
const AUTH_TOKEN_QUERY = "auth_token"
|
||||
const WWW_AUTHENTICATE = 'Basic realm="Secure Area"'
|
||||
@@ -78,10 +78,16 @@ export const authorizationLayer = Layer.effect(
|
||||
return Authorization.of((effect) =>
|
||||
Effect.gen(function* () {
|
||||
const request = yield* HttpServerRequest.HttpServerRequest
|
||||
const route = yield* HttpRouter.RouteContext
|
||||
// Ticketed PTY connects (browsers cannot set headers on WebSocket upgrades) and pairing links
|
||||
// skip credential checks here; their handlers consume and validate the ticket or code.
|
||||
// skip credential checks here; their handlers consume and validate the ticket or code. Use the
|
||||
// route selected by Effect so authorization and dispatch cannot disagree about the request path.
|
||||
const url = new URL(request.url, "http://localhost")
|
||||
if (hasPtyConnectTicketURL(url) || hasPersistentPtyConnectTicketURL(url) || isPairingConnectURL(url))
|
||||
if (
|
||||
((route.route.path === PTY_CONNECT_PATH || route.route.path === PERSISTENT_PTY_CONNECT_PATH) &&
|
||||
!!url.searchParams.get(PTY_CONNECT_TICKET_QUERY)) ||
|
||||
route.route.path === PAIRING_CONNECT_PATH
|
||||
)
|
||||
return yield* effect
|
||||
if (yield* authorizedRequest(request, config)) return yield* effect
|
||||
if (challengeRequest(request))
|
||||
|
||||
Reference in New Issue
Block a user