瀏覽代碼

fix(mcp): bind oauth callback to IPv4 loopback (#30022)

Yufeng He 2 月之前
父節點
當前提交
af31e97493
共有 2 個文件被更改,包括 45 次插入1 次删除
  1. 3 1
      packages/opencode/src/mcp/oauth-callback.ts
  2. 42 0
      packages/opencode/test/mcp/oauth-callback.test.ts

+ 3 - 1
packages/opencode/src/mcp/oauth-callback.ts

@@ -3,6 +3,8 @@ import { createServer } from "http"
 import { escapeHtml } from "@/util/html"
 import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH, parseRedirectUri } from "./oauth-provider"
 
+const OAUTH_CALLBACK_HOST = "127.0.0.1"
+
 // Current callback server configuration (may differ from defaults if custom redirectUri is used)
 let currentPort = OAUTH_CALLBACK_PORT
 let currentPath = OAUTH_CALLBACK_PATH
@@ -162,7 +164,7 @@ export async function ensureRunning(redirectUri?: string): Promise<void> {
 
   server = createServer(handleRequest)
   await new Promise<void>((resolve, reject) => {
-    server!.listen(currentPort, () => {
+    server!.listen(currentPort, OAUTH_CALLBACK_HOST, () => {
       resolve()
     })
     server!.on("error", reject)

+ 42 - 0
packages/opencode/test/mcp/oauth-callback.test.ts

@@ -1,7 +1,41 @@
 import { test, expect, describe, afterEach } from "bun:test"
+import { createConnection, createServer as createNetServer } from "net"
 import { McpOAuthCallback } from "../../src/mcp/oauth-callback"
 import { parseRedirectUri } from "../../src/mcp/oauth-provider"
 
+async function getFreeLoopbackPort(): Promise<number> {
+  return new Promise((resolve, reject) => {
+    const probe = createNetServer()
+    probe.once("error", reject)
+    probe.listen(0, "127.0.0.1", () => {
+      const address = probe.address()
+      probe.close(() => {
+        if (typeof address === "object" && address) {
+          resolve(address.port)
+          return
+        }
+        reject(new Error("Could not allocate a loopback port"))
+      })
+    })
+  })
+}
+
+async function canConnect(host: string, port: number): Promise<boolean> {
+  return new Promise((resolve) => {
+    const socket = createConnection({ host, port })
+    const done = (ok: boolean) => {
+      socket.removeAllListeners()
+      socket.destroy()
+      resolve(ok)
+    }
+
+    socket.setTimeout(500)
+    socket.once("connect", () => done(true))
+    socket.once("error", () => done(false))
+    socket.once("timeout", () => done(false))
+  })
+}
+
 describe("parseRedirectUri", () => {
   test("returns defaults when no URI provided", () => {
     const result = parseRedirectUri()
@@ -69,4 +103,12 @@ describe("McpOAuthCallback.ensureRunning", () => {
 
     expect(await response.text()).toContain('<div class="error">The user denied access</div>')
   })
+
+  test("binds the callback server to IPv4 loopback", async () => {
+    const port = await getFreeLoopbackPort()
+    await McpOAuthCallback.ensureRunning(`http://127.0.0.1:${port}/custom/callback`)
+
+    expect(await canConnect("127.0.0.1", port)).toBe(true)
+    expect(await canConnect("::1", port)).toBe(false)
+  })
 })