Skip to content

Commit c45e425

Browse files
authored
fix(server): require authentication for frontend requests (#48217)
1 parent 7fb7938 commit c45e425

3 files changed

Lines changed: 52 additions & 4 deletions

File tree

‎packages/server/src/process.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,6 @@ function dispatch(
193193
const app = yield* Ref.get(application)
194194
const ready = state.type === "ready" && Option.isSome(app)
195195
if (
196-
(url.pathname === "/api" || url.pathname.startsWith("/api/") || url.pathname === "/openapi.json") &&
197196
(!ready || (!hasPtyConnectTicketURL(url) && !hasPersistentPtyConnectTicketURL(url))) &&
198197
!(yield* authorizedRequest(request, auth))
199198
)

‎packages/server/test/process.test.ts‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { HttpServer, HttpServerError, HttpServerResponse } from "effect/unstable
44
import { it } from "../../core/test/lib/effect"
55
import { ServerProcess } from "../src/process"
66

7-
it.live("allows browser preflight requests without credentials", () =>
7+
it.live("authenticates API and frontend requests while allowing browser preflight", () =>
88
Effect.gen(function* () {
99
const fallback = "fallback".repeat(256)
1010
const server = yield* ServerProcess.start<never, never>(
@@ -130,6 +130,36 @@ it.live("allows browser preflight requests without credentials", () =>
130130
expect(yield* Effect.promise(() => response.text())).toBe("")
131131
}),
132132
)
133+
134+
yield* Effect.forEach(["/", "/workspace/example", "/_assets/app.js", "/icons/icon.svg", "/sw.js"], (pathname) =>
135+
Effect.gen(function* () {
136+
yield* Effect.forEach(["GET", "HEAD"], (method) =>
137+
Effect.gen(function* () {
138+
yield* Effect.forEach([undefined, `Basic ${btoa("opencode:wrong")}`], (authorization) =>
139+
Effect.gen(function* () {
140+
const response = yield* Effect.promise(() =>
141+
fetch(new URL(pathname, HttpServer.formatAddress(server.address)), {
142+
method,
143+
headers: authorization ? { authorization } : undefined,
144+
}),
145+
)
146+
expect(response.status).toBe(401)
147+
expect(response.headers.get("www-authenticate")).toBe('Basic realm="Secure Area"')
148+
expect(yield* Effect.promise(() => response.text())).toBe("")
149+
}),
150+
)
151+
const response = yield* Effect.promise(() =>
152+
fetch(new URL(pathname, HttpServer.formatAddress(server.address)), {
153+
method,
154+
headers: { authorization: `Basic ${btoa("opencode:secret")}` },
155+
}),
156+
)
157+
expect(response.status).toBe(200)
158+
expect(yield* Effect.promise(() => response.text())).toBe(method === "HEAD" ? "" : fallback)
159+
}),
160+
)
161+
}),
162+
)
133163
}),
134164
)
135165

‎packages/tui/src/component/dialog-pair.tsx‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { renderUnicodeCompact } from "uqr"
55
import { useClient } from "../context/client"
66
import { useTheme } from "../context/theme"
77
import { useDialog } from "../ui/dialog"
8+
import { Link } from "../ui/link"
89
import { errorMessage } from "../util/error"
910

1011
export type DialogPairCredentials = {
@@ -50,16 +51,34 @@ export function DialogPair(props: { credentials?: DialogPairCredentials }) {
5051
const content = () => {
5152
const value = info()
5253
if (!value) return
54+
const href = (input: string) => {
55+
const url = new URL(input)
56+
url.username = encodeURIComponent(value.username)
57+
url.password = encodeURIComponent(value.password)
58+
return url.toString()
59+
}
5360
return (
5461
<box flexDirection={horizontal() ? "row" : "column"} alignItems={horizontal() ? "flex-start" : "center"} gap={2}>
5562
<box width={horizontal() ? 29 : "100%"} flexShrink={0} gap={1}>
5663
<box>
5764
<text fg={theme.text.subdued}>This device</text>
58-
<text fg={theme.text.default}>{localhost()}</text>
65+
<Show when={localhost()}>
66+
{(url) => (
67+
<Link href={href(url())} fg={theme.text.default}>
68+
{url()}
69+
</Link>
70+
)}
71+
</Show>
5972
</box>
6073
<box>
6174
<text fg={theme.text.subdued}>URLs</text>
62-
<For each={value.urls}>{(url) => <text fg={theme.text.default}>{url}</text>}</For>
75+
<For each={value.urls}>
76+
{(url) => (
77+
<Link href={href(url)} fg={theme.text.default}>
78+
{url}
79+
</Link>
80+
)}
81+
</For>
6382
</box>
6483
<box>
6584
<text fg={theme.text.subdued}>Username</text>

0 commit comments

Comments
 (0)