Skip to content
Open
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
9 changes: 4 additions & 5 deletions packages/protocol/src/groups/pty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ import { LocationQuery, locationQueryOpenApi } from "./location"
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 PtyGroup = HttpApiGroup.make("server.pty")
Expand Down Expand Up @@ -116,7 +115,7 @@ export const PtyGroup = HttpApiGroup.make("server.pty")
.add(
// Query fields are decoded in the raw handler after the existence check so a missing
// session responds with an empty 404 before any 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],
Expand Down
12 changes: 8 additions & 4 deletions packages/server/src/middleware/authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { ServerAuth } from "../auth"
import { UnauthorizedError } from "@opencode-ai/protocol/errors"
import { Authorization } from "@opencode-ai/protocol/middleware/authorization"
export { Authorization } from "@opencode-ai/protocol/middleware/authorization"
import { hasPtyConnectTicketURL } from "@opencode-ai/protocol/groups/pty"
import { PTY_CONNECT_PATH, PTY_CONNECT_TICKET_QUERY } from "@opencode-ai/protocol/groups/pty"
import { Effect, Encoding, Layer, Redacted } from "effect"
import { HttpEffect, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
import { HttpEffect, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"

const AUTH_TOKEN_QUERY = "auth_token"
const WWW_AUTHENTICATE = 'Basic realm="Secure Area"'
Expand Down Expand Up @@ -43,9 +43,13 @@ export const authorizationLayer = Layer.effect(
return Authorization.of((effect) =>
Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest
const route = yield* HttpRouter.RouteContext
// Browsers cannot set headers on WebSocket upgrades, so a ticketed PTY connect skips
// credential checks here; the connect handler consumes and validates the ticket.
if (hasPtyConnectTicketURL(new URL(request.url, "http://localhost"))) return yield* effect
// credential checks here; the connect handler consumes and validates the ticket. 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 (route.route.path === PTY_CONNECT_PATH && !!url.searchParams.get(PTY_CONNECT_TICKET_QUERY))
return yield* effect
const credential = yield* credentialFromRequest(request)
if (ServerAuth.authorized(credential, config)) return yield* effect
yield* HttpEffect.appendPreResponseHandler((_request, response) =>
Expand Down
Loading