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 () => {
|
||||
const server = Server.listen({ port: args.port ?? 0, hostname: "127.0.0.1" })
|
||||
const sdk = createOpencodeClient({ baseUrl: `http://${server.hostname}:${server.port}` })
|
||||
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
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) {
|
||||
const exists = await Command.get(args.command)
|
||||
if (!exists) {
|
||||
server.stop()
|
||||
UI.error(`Command "${args.command}" not found`)
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -370,7 +372,6 @@ export const RunCommand = cmd({
|
||||
})()
|
||||
|
||||
if (!sessionID) {
|
||||
server.stop()
|
||||
UI.error("Session not found")
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -389,7 +390,6 @@ export const RunCommand = cmd({
|
||||
}
|
||||
|
||||
await execute(sdk, sessionID)
|
||||
server.stop()
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { Server } from "../../server/server"
|
||||
import { cmd } from "./cmd"
|
||||
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
||||
import { Flag } from "../../flag/flag"
|
||||
|
||||
export const ServeCommand = cmd({
|
||||
command: "serve",
|
||||
builder: (yargs) => withNetworkOptions(yargs),
|
||||
describe: "starts a headless opencode server",
|
||||
handler: async (args) => {
|
||||
if (!Flag.OPENCODE_PASSWORD) {
|
||||
console.log("Warning: OPENCODE_PASSWORD is not set; server is unsecured.")
|
||||
}
|
||||
const opts = await resolveNetworkOptions(args)
|
||||
const server = Server.listen(opts)
|
||||
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 { cmd } from "./cmd"
|
||||
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
||||
import { Flag } from "../../flag/flag"
|
||||
import open from "open"
|
||||
import { networkInterfaces } from "os"
|
||||
|
||||
@@ -32,6 +33,9 @@ export const WebCommand = cmd({
|
||||
builder: (yargs) => withNetworkOptions(yargs),
|
||||
describe: "start opencode server and open web interface",
|
||||
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 server = Server.listen(opts)
|
||||
UI.empty()
|
||||
|
||||
@@ -20,6 +20,7 @@ export namespace Flag {
|
||||
OPENCODE_DISABLE_CLAUDE_CODE || truthy("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS")
|
||||
export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
|
||||
export const OPENCODE_CLIENT = process.env["OPENCODE_CLIENT"] ?? "cli"
|
||||
export const OPENCODE_PASSWORD = process.env["OPENCODE_PASSWORD"]
|
||||
|
||||
// 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 { AttachCommand } from "./cli/cmd/tui/attach"
|
||||
import { TuiThreadCommand } from "./cli/cmd/tui/thread"
|
||||
import { TuiSpawnCommand } from "./cli/cmd/tui/spawn"
|
||||
import { AcpCommand } from "./cli/cmd/acp"
|
||||
import { EOL } from "os"
|
||||
import { WebCommand } from "./cli/cmd/web"
|
||||
@@ -81,7 +80,6 @@ const cli = yargs(hideBin(process.argv))
|
||||
.command(AcpCommand)
|
||||
.command(McpCommand)
|
||||
.command(TuiThreadCommand)
|
||||
.command(TuiSpawnCommand)
|
||||
.command(AttachCommand)
|
||||
.command(RunCommand)
|
||||
.command(GenerateCommand)
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Hono } from "hono"
|
||||
import { cors } from "hono/cors"
|
||||
import { stream, streamSSE } from "hono/streaming"
|
||||
import { proxy } from "hono/proxy"
|
||||
import { basicAuth } from "hono/basic-auth"
|
||||
import { Session } from "../session"
|
||||
import z from "zod"
|
||||
import { Provider } from "../provider/provider"
|
||||
@@ -25,6 +26,7 @@ import { Project } from "../project/project"
|
||||
import { Vcs } from "../project/vcs"
|
||||
import { Agent } from "../agent/agent"
|
||||
import { Auth } from "../auth"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { Command } from "../command"
|
||||
import { ProviderAuth } from "../provider/auth"
|
||||
import { Global } from "../global"
|
||||
@@ -45,6 +47,7 @@ import { Snapshot } from "@/snapshot"
|
||||
import { SessionSummary } from "@/session/summary"
|
||||
import { SessionStatus } from "@/session/status"
|
||||
import { upgradeWebSocket, websocket } from "hono/bun"
|
||||
import { HTTPException } from "hono/http-exception"
|
||||
import { errors } from "./error"
|
||||
import { Pty } from "@/pty"
|
||||
import { PermissionNext } from "@/permission/next"
|
||||
@@ -80,6 +83,7 @@ export namespace Server {
|
||||
log.error("failed", {
|
||||
error: err,
|
||||
})
|
||||
if (err instanceof HTTPException) return err.getResponse()
|
||||
if (err instanceof NamedError) {
|
||||
let status: ContentfulStatusCode
|
||||
if (err instanceof Storage.NotFoundError) status = 404
|
||||
@@ -93,6 +97,11 @@ export namespace Server {
|
||||
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) => {
|
||||
const skipLogging = c.req.path === "/log"
|
||||
if (!skipLogging) {
|
||||
|
||||
Reference in New Issue
Block a user