proxy.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import { Hono } from "hono"
  2. import type { UpgradeWebSocket } from "hono/ws"
  3. import { Log } from "@/util/log"
  4. const hop = new Set([
  5. "connection",
  6. "keep-alive",
  7. "proxy-authenticate",
  8. "proxy-authorization",
  9. "proxy-connection",
  10. "te",
  11. "trailer",
  12. "transfer-encoding",
  13. "upgrade",
  14. "host",
  15. ])
  16. type Msg = string | ArrayBuffer | Uint8Array
  17. function headers(req: Request, extra?: HeadersInit) {
  18. const out = new Headers(req.headers)
  19. for (const key of hop) out.delete(key)
  20. out.delete("accept-encoding")
  21. out.delete("x-opencode-directory")
  22. out.delete("x-opencode-workspace")
  23. if (!extra) return out
  24. for (const [key, value] of new Headers(extra).entries()) {
  25. out.set(key, value)
  26. }
  27. return out
  28. }
  29. function protocols(req: Request) {
  30. const value = req.headers.get("sec-websocket-protocol")
  31. if (!value) return []
  32. return value
  33. .split(",")
  34. .map((item) => item.trim())
  35. .filter(Boolean)
  36. }
  37. function socket(url: string | URL) {
  38. const next = new URL(url)
  39. if (next.protocol === "http:") next.protocol = "ws:"
  40. if (next.protocol === "https:") next.protocol = "wss:"
  41. return next.toString()
  42. }
  43. function send(ws: { send(data: string | ArrayBuffer | Uint8Array): void }, data: any) {
  44. if (data instanceof Blob) {
  45. return data.arrayBuffer().then((x) => ws.send(x))
  46. }
  47. return ws.send(data)
  48. }
  49. const app = (upgrade: UpgradeWebSocket) =>
  50. new Hono().get(
  51. "/__workspace_ws",
  52. upgrade((c) => {
  53. const url = c.req.header("x-opencode-proxy-url")
  54. const queue: Msg[] = []
  55. let remote: WebSocket | undefined
  56. return {
  57. onOpen(_, ws) {
  58. if (!url) {
  59. ws.close(1011, "missing proxy target")
  60. return
  61. }
  62. remote = new WebSocket(url, protocols(c.req.raw))
  63. remote.binaryType = "arraybuffer"
  64. remote.onopen = () => {
  65. for (const item of queue) remote?.send(item)
  66. queue.length = 0
  67. }
  68. remote.onmessage = (event) => {
  69. send(ws, event.data)
  70. }
  71. remote.onerror = () => {
  72. ws.close(1011, "proxy error")
  73. }
  74. remote.onclose = (event) => {
  75. ws.close(event.code, event.reason)
  76. }
  77. },
  78. onMessage(event) {
  79. const data = event.data
  80. if (typeof data !== "string" && !(data instanceof Uint8Array) && !(data instanceof ArrayBuffer)) return
  81. if (remote?.readyState === WebSocket.OPEN) {
  82. remote.send(data)
  83. return
  84. }
  85. queue.push(data)
  86. },
  87. onClose(event) {
  88. remote?.close(event.code, event.reason)
  89. },
  90. }
  91. }),
  92. )
  93. export namespace ServerProxy {
  94. const log = Log.Default.clone().tag("service", "server-proxy")
  95. export function http(url: string | URL, extra: HeadersInit | undefined, req: Request) {
  96. console.log("proxy http request", {
  97. method: req.method,
  98. request: req.url,
  99. url: String(url),
  100. })
  101. return fetch(
  102. new Request(url, {
  103. method: req.method,
  104. headers: headers(req, extra),
  105. body: req.method === "GET" || req.method === "HEAD" ? undefined : req.body,
  106. redirect: "manual",
  107. signal: req.signal,
  108. }),
  109. ).then((res) => {
  110. const next = new Headers(res.headers)
  111. next.delete("content-encoding")
  112. next.delete("content-length")
  113. console.log("proxy http response", {
  114. method: req.method,
  115. request: req.url,
  116. url: String(url),
  117. status: res.status,
  118. statusText: res.statusText,
  119. })
  120. return new Response(res.body, {
  121. status: res.status,
  122. statusText: res.statusText,
  123. headers: next,
  124. })
  125. })
  126. }
  127. export function websocket(
  128. upgrade: UpgradeWebSocket,
  129. target: string | URL,
  130. extra: HeadersInit | undefined,
  131. req: Request,
  132. env: unknown,
  133. ) {
  134. const proxy = new URL(req.url)
  135. proxy.pathname = "/__workspace_ws"
  136. proxy.search = ""
  137. const next = new Headers(req.headers)
  138. next.set("x-opencode-proxy-url", socket(target))
  139. for (const [key, value] of new Headers(extra).entries()) {
  140. next.set(key, value)
  141. }
  142. log.info("proxy websocket", {
  143. request: req.url,
  144. target: String(target),
  145. })
  146. return app(upgrade).fetch(
  147. new Request(proxy, {
  148. method: req.method,
  149. headers: next,
  150. signal: req.signal,
  151. }),
  152. env as never,
  153. )
  154. }
  155. }