core: add password authentication and improve server security
- Add OPENCODE_PASSWORD flag for basic auth protection - Show security warnings when password is not set - Remove deprecated spawn command - Improve error handling with HTTPException responses
This commit is contained in:
@@ -339,13 +339,15 @@ export const RunCommand = cmd({
|
|||||||
}
|
}
|
||||||
|
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const server = Server.listen({ port: args.port ?? 0, hostname: "127.0.0.1" })
|
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
const sdk = createOpencodeClient({ baseUrl: `http://${server.hostname}:${server.port}` })
|
const request = new Request(input, init)
|
||||||
|
return Server.App().fetch(request)
|
||||||
|
}) as typeof globalThis.fetch
|
||||||
|
const sdk = createOpencodeClient({ baseUrl: "http://opencode.internal", fetch: fetchFn })
|
||||||
|
|
||||||
if (args.command) {
|
if (args.command) {
|
||||||
const exists = await Command.get(args.command)
|
const exists = await Command.get(args.command)
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
server.stop()
|
|
||||||
UI.error(`Command "${args.command}" not found`)
|
UI.error(`Command "${args.command}" not found`)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
@@ -370,7 +372,6 @@ export const RunCommand = cmd({
|
|||||||
})()
|
})()
|
||||||
|
|
||||||
if (!sessionID) {
|
if (!sessionID) {
|
||||||
server.stop()
|
|
||||||
UI.error("Session not found")
|
UI.error("Session not found")
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
@@ -389,7 +390,6 @@ export const RunCommand = cmd({
|
|||||||
}
|
}
|
||||||
|
|
||||||
await execute(sdk, sessionID)
|
await execute(sdk, sessionID)
|
||||||
server.stop()
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { Server } from "../../server/server"
|
import { Server } from "../../server/server"
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
||||||
|
import { Flag } from "../../flag/flag"
|
||||||
|
|
||||||
export const ServeCommand = cmd({
|
export const ServeCommand = cmd({
|
||||||
command: "serve",
|
command: "serve",
|
||||||
builder: (yargs) => withNetworkOptions(yargs),
|
builder: (yargs) => withNetworkOptions(yargs),
|
||||||
describe: "starts a headless opencode server",
|
describe: "starts a headless opencode server",
|
||||||
handler: async (args) => {
|
handler: async (args) => {
|
||||||
|
if (!Flag.OPENCODE_PASSWORD) {
|
||||||
|
console.log("Warning: OPENCODE_PASSWORD is not set; server is unsecured.")
|
||||||
|
}
|
||||||
const opts = await resolveNetworkOptions(args)
|
const opts = await resolveNetworkOptions(args)
|
||||||
const server = Server.listen(opts)
|
const server = Server.listen(opts)
|
||||||
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
|
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
import { cmd } from "@/cli/cmd/cmd"
|
|
||||||
import { Instance } from "@/project/instance"
|
|
||||||
import path from "path"
|
|
||||||
import { Server } from "@/server/server"
|
|
||||||
import { upgrade } from "@/cli/upgrade"
|
|
||||||
import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
|
|
||||||
|
|
||||||
export const TuiSpawnCommand = cmd({
|
|
||||||
command: "spawn [project]",
|
|
||||||
builder: (yargs) =>
|
|
||||||
withNetworkOptions(yargs).positional("project", {
|
|
||||||
type: "string",
|
|
||||||
describe: "path to start opencode in",
|
|
||||||
}),
|
|
||||||
handler: async (args) => {
|
|
||||||
upgrade()
|
|
||||||
const opts = await resolveNetworkOptions(args)
|
|
||||||
const server = Server.listen(opts)
|
|
||||||
const bin = process.execPath
|
|
||||||
const cmd = []
|
|
||||||
let cwd = process.cwd()
|
|
||||||
if (bin.endsWith("bun")) {
|
|
||||||
cmd.push(
|
|
||||||
process.execPath,
|
|
||||||
"run",
|
|
||||||
"--conditions",
|
|
||||||
"browser",
|
|
||||||
new URL("../../../index.ts", import.meta.url).pathname,
|
|
||||||
)
|
|
||||||
cwd = new URL("../../../../", import.meta.url).pathname
|
|
||||||
} else cmd.push(process.execPath)
|
|
||||||
cmd.push("attach", server.url.toString(), "--dir", args.project ? path.resolve(args.project) : process.cwd())
|
|
||||||
const proc = Bun.spawn({
|
|
||||||
cmd,
|
|
||||||
cwd,
|
|
||||||
stdout: "inherit",
|
|
||||||
stderr: "inherit",
|
|
||||||
stdin: "inherit",
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
BUN_OPTIONS: "",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await proc.exited
|
|
||||||
await Instance.disposeAll()
|
|
||||||
await server.stop(true)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -2,6 +2,7 @@ import { Server } from "../../server/server"
|
|||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
||||||
|
import { Flag } from "../../flag/flag"
|
||||||
import open from "open"
|
import open from "open"
|
||||||
import { networkInterfaces } from "os"
|
import { networkInterfaces } from "os"
|
||||||
|
|
||||||
@@ -32,6 +33,9 @@ export const WebCommand = cmd({
|
|||||||
builder: (yargs) => withNetworkOptions(yargs),
|
builder: (yargs) => withNetworkOptions(yargs),
|
||||||
describe: "start opencode server and open web interface",
|
describe: "start opencode server and open web interface",
|
||||||
handler: async (args) => {
|
handler: async (args) => {
|
||||||
|
if (!Flag.OPENCODE_PASSWORD) {
|
||||||
|
UI.println(UI.Style.TEXT_WARNING_BOLD + "! " + "OPENCODE_PASSWORD is not set; server is unsecured.")
|
||||||
|
}
|
||||||
const opts = await resolveNetworkOptions(args)
|
const opts = await resolveNetworkOptions(args)
|
||||||
const server = Server.listen(opts)
|
const server = Server.listen(opts)
|
||||||
UI.empty()
|
UI.empty()
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export namespace Flag {
|
|||||||
OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS")
|
OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS")
|
||||||
export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
|
export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
|
||||||
export const OPENCODE_CLIENT = process.env["OPENCODE_CLIENT"] ?? "cli"
|
export const OPENCODE_CLIENT = process.env["OPENCODE_CLIENT"] ?? "cli"
|
||||||
|
export const OPENCODE_PASSWORD = process.env["OPENCODE_PASSWORD"]
|
||||||
|
|
||||||
// Experimental
|
// Experimental
|
||||||
export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL")
|
export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL")
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import { ExportCommand } from "./cli/cmd/export"
|
|||||||
import { ImportCommand } from "./cli/cmd/import"
|
import { ImportCommand } from "./cli/cmd/import"
|
||||||
import { AttachCommand } from "./cli/cmd/tui/attach"
|
import { AttachCommand } from "./cli/cmd/tui/attach"
|
||||||
import { TuiThreadCommand } from "./cli/cmd/tui/thread"
|
import { TuiThreadCommand } from "./cli/cmd/tui/thread"
|
||||||
import { TuiSpawnCommand } from "./cli/cmd/tui/spawn"
|
|
||||||
import { AcpCommand } from "./cli/cmd/acp"
|
import { AcpCommand } from "./cli/cmd/acp"
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { WebCommand } from "./cli/cmd/web"
|
import { WebCommand } from "./cli/cmd/web"
|
||||||
@@ -81,7 +80,6 @@ const cli = yargs(hideBin(process.argv))
|
|||||||
.command(AcpCommand)
|
.command(AcpCommand)
|
||||||
.command(McpCommand)
|
.command(McpCommand)
|
||||||
.command(TuiThreadCommand)
|
.command(TuiThreadCommand)
|
||||||
.command(TuiSpawnCommand)
|
|
||||||
.command(AttachCommand)
|
.command(AttachCommand)
|
||||||
.command(RunCommand)
|
.command(RunCommand)
|
||||||
.command(GenerateCommand)
|
.command(GenerateCommand)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Hono } from "hono"
|
|||||||
import { cors } from "hono/cors"
|
import { cors } from "hono/cors"
|
||||||
import { stream, streamSSE } from "hono/streaming"
|
import { stream, streamSSE } from "hono/streaming"
|
||||||
import { proxy } from "hono/proxy"
|
import { proxy } from "hono/proxy"
|
||||||
|
import { basicAuth } from "hono/basic-auth"
|
||||||
import { Session } from "../session"
|
import { Session } from "../session"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Provider } from "../provider/provider"
|
import { Provider } from "../provider/provider"
|
||||||
@@ -25,6 +26,7 @@ import { Project } from "../project/project"
|
|||||||
import { Vcs } from "../project/vcs"
|
import { Vcs } from "../project/vcs"
|
||||||
import { Agent } from "../agent/agent"
|
import { Agent } from "../agent/agent"
|
||||||
import { Auth } from "../auth"
|
import { Auth } from "../auth"
|
||||||
|
import { Flag } from "../flag/flag"
|
||||||
import { Command } from "../command"
|
import { Command } from "../command"
|
||||||
import { ProviderAuth } from "../provider/auth"
|
import { ProviderAuth } from "../provider/auth"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
@@ -45,6 +47,7 @@ import { Snapshot } from "@/snapshot"
|
|||||||
import { SessionSummary } from "@/session/summary"
|
import { SessionSummary } from "@/session/summary"
|
||||||
import { SessionStatus } from "@/session/status"
|
import { SessionStatus } from "@/session/status"
|
||||||
import { upgradeWebSocket, websocket } from "hono/bun"
|
import { upgradeWebSocket, websocket } from "hono/bun"
|
||||||
|
import { HTTPException } from "hono/http-exception"
|
||||||
import { errors } from "./error"
|
import { errors } from "./error"
|
||||||
import { Pty } from "@/pty"
|
import { Pty } from "@/pty"
|
||||||
import { PermissionNext } from "@/permission/next"
|
import { PermissionNext } from "@/permission/next"
|
||||||
@@ -80,6 +83,7 @@ export namespace Server {
|
|||||||
log.error("failed", {
|
log.error("failed", {
|
||||||
error: err,
|
error: err,
|
||||||
})
|
})
|
||||||
|
if (err instanceof HTTPException) return err.getResponse()
|
||||||
if (err instanceof NamedError) {
|
if (err instanceof NamedError) {
|
||||||
let status: ContentfulStatusCode
|
let status: ContentfulStatusCode
|
||||||
if (err instanceof Storage.NotFoundError) status = 404
|
if (err instanceof Storage.NotFoundError) status = 404
|
||||||
@@ -93,6 +97,11 @@ export namespace Server {
|
|||||||
status: 500,
|
status: 500,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
.use((c, next) => {
|
||||||
|
const password = Flag.OPENCODE_PASSWORD
|
||||||
|
if (!password) return next()
|
||||||
|
return basicAuth({ username: "opencode", password })(c, next)
|
||||||
|
})
|
||||||
.use(async (c, next) => {
|
.use(async (c, next) => {
|
||||||
const skipLogging = c.req.path === "/log"
|
const skipLogging = c.req.path === "/log"
|
||||||
if (!skipLogging) {
|
if (!skipLogging) {
|
||||||
|
|||||||
Reference in New Issue
Block a user