Skip to content

Commit dcb654c

Browse files
committed
Enable port range support for local WebSocket server connections and validate chatbot URLs
├── apps │ ├── browser │ │ ├── README.md │ │ └── src │ │ ├── background │ │ │ ├── keep-alive.ts │ │ │ ├── main.ts │ │ │ ├── message-handler.ts │ │ │ └── websocket.ts │ │ └── manifest.json │ └── editor │ └── src │ ├── i18n │ │ └── translations │ │ └── services │ │ └── websocket-manager.ts │ └── services │ └── websocket-manager.ts ├── docs │ └── privacy.mdx └── packages └── shared └── src ├── constants │ └── websocket.ts └── types └── websocket-message.ts
1 parent c4eab0a commit dcb654c

8 files changed

Lines changed: 197 additions & 89 deletions

File tree

‎apps/browser/README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ The extension functions with minimal permissions.
2727

2828
- `storage` - Used to temporarily store prompts for [a content script](https://github.com/robertpiosik/CodeWebChat/tree/dev/apps/browser/src/content-scripts/send-prompt-content-script), and to save extension settings.
2929
- `alarms` (Chrome only) - Used to implement a keep-alive mechanism for the background service worker to ensure the WebSocket connection stays active.
30-
- `host_permissions` (localhost) - Required to connect to the local WebSocket server (`ws://localhost:55155`), managed by the VS Code extension.
30+
31+
The extension requests no host permissions. It connects to the local WebSocket server managed by the VS Code extension (`ws://localhost:55155`, or the next few ports up to `55164` when a forwarded remote port had to be shifted).
3132

3233
Firefox only:
3334

‎apps/browser/src/background/main.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { connect_websocket } from './websocket'
1+
import { check_and_recover_connection } from './websocket'
22
import { setup_keep_alive } from './keep-alive'
33
import { setup_message_listeners } from './message-handler'
44
import { clear_chat_init_data } from './clear-chat-init-data'
55

66
async function init() {
77
await clear_chat_init_data()
8-
connect_websocket()
8+
check_and_recover_connection()
99
setup_keep_alive()
1010
setup_message_listeners()
1111
}

‎apps/browser/src/background/message-handler.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
import browser from 'webextension-polyfill'
77
import { send_message_to_server } from './websocket'
88
import { is_message } from '@/utils/is-message'
9+
import { CHATBOTS } from '@shared/constants/chatbots'
910

1011
interface ChatQueueItem {
1112
message: InitializeChatMessage
@@ -157,6 +158,34 @@ const start_processing = async () => {
157158
const handle_initialize_chat_message = async (
158159
message: InitializeChatMessage
159160
) => {
161+
try {
162+
const parsed_url = new URL(message.url)
163+
let is_allowed = parsed_url.hostname === 'openwebui'
164+
165+
if (!is_allowed) {
166+
for (const key of Object.keys(CHATBOTS)) {
167+
const chatbot = CHATBOTS[key as keyof typeof CHATBOTS] as any
168+
try {
169+
if (chatbot?.url) {
170+
const chatbot_url = new URL(chatbot.url)
171+
if (parsed_url.hostname === chatbot_url.hostname) {
172+
is_allowed = true
173+
break
174+
}
175+
}
176+
} catch {}
177+
}
178+
}
179+
180+
if (!is_allowed) {
181+
console.warn(`Blocked initialize-chat with disallowed URL: ${message.url}`)
182+
return
183+
}
184+
} catch {
185+
console.warn(`Blocked initialize-chat with invalid URL: ${message.url}`)
186+
return
187+
}
188+
160189
chat_queue.push({ message })
161190
await start_processing()
162191
}

‎apps/browser/src/background/websocket.ts‎

Lines changed: 130 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -2,72 +2,85 @@ import { WebSocketMessage } from '@shared/types/websocket-message'
22
import { handle_messages } from './message-handler'
33
import { DEFAULT_PORT, CLIENT_TYPE } from '@shared/constants/websocket'
44
import browser from 'webextension-polyfill'
5-
let websocket: WebSocket | null = null
6-
let is_reconnecting = false
7-
let last_ping_timestamp = Date.now()
85

9-
browser.storage.onChanged.addListener((changes, areaName) => {
10-
if (areaName === 'local' && changes['browser-profile-name']) {
11-
if (websocket) {
12-
is_reconnecting = false
13-
const old_ws = websocket
14-
websocket = null
15-
old_ws.close()
16-
}
17-
connect_websocket()
6+
// Remote setups (Remote-SSH, WSL, Dev Containers) may forward the server to
7+
// DEFAULT_PORT + n when the local DEFAULT_PORT is already occupied.
8+
const PORT_RANGE_SIZE = 10
9+
10+
const ports_to_check = (): number[] => {
11+
const ports = []
12+
for (let i = 0; i < PORT_RANGE_SIZE; i++) {
13+
ports.push(DEFAULT_PORT + i)
1814
}
19-
})
15+
return ports
16+
}
2017

21-
export const check_server_health = async (): Promise<boolean> => {
22-
try {
23-
const response = await fetch(`http://localhost:${DEFAULT_PORT}/health`)
24-
return response.ok
25-
} catch {
26-
return false
18+
const STALE_AFTER_MS = 20000
19+
const RETRY_AFTER_DROP_MS = 1000 // we had a live connection, retry quickly
20+
const RETRY_IDLE_MS = 5000 // nothing was ever listening, poll slowly
21+
22+
type Connection = {
23+
ws: WebSocket | null
24+
last_ping: number
25+
retry_timer: ReturnType<typeof setTimeout> | null
26+
}
27+
28+
const connections = new Map<number, Connection>()
29+
30+
const client_id_to_port = new Map<string, number>()
31+
32+
const get_connection = (port: number): Connection => {
33+
let c = connections.get(port)
34+
if (!c) {
35+
c = { ws: null, last_ping: Date.now(), retry_timer: null }
36+
connections.set(port, c)
2737
}
38+
return c
39+
}
40+
41+
const schedule_retry = (port: number, delay_ms: number) => {
42+
const c = get_connection(port)
43+
if (c.retry_timer) return
44+
c.retry_timer = setTimeout(() => {
45+
c.retry_timer = null
46+
connect_websocket(port)
47+
}, delay_ms)
2848
}
2949

50+
const is_busy = (c: Connection) =>
51+
c.ws?.readyState === WebSocket.OPEN ||
52+
c.ws?.readyState === WebSocket.CONNECTING
53+
3054
export const check_and_recover_connection = () => {
31-
if (websocket?.readyState === WebSocket.OPEN) {
32-
if (Date.now() - last_ping_timestamp > 20000) {
33-
console.warn(
34-
'WebSocket connection is stale (no recent pings). Force reconnecting...'
35-
)
36-
is_reconnecting = false
37-
websocket.close()
55+
for (const port of ports_to_check()) {
56+
const c = get_connection(port)
57+
58+
if (c.ws?.readyState === WebSocket.OPEN) {
59+
if (Date.now() - c.last_ping > STALE_AFTER_MS) {
60+
console.warn(
61+
`WebSocket on port ${port} is stale (no recent pings). Force reconnecting...`
62+
)
63+
c.ws.close()
64+
}
65+
continue
3866
}
39-
}
4067

41-
if (
42-
!is_reconnecting &&
43-
websocket?.readyState !== WebSocket.OPEN &&
44-
websocket?.readyState !== WebSocket.CONNECTING
45-
) {
46-
connect_websocket()
68+
if (!is_busy(c) && !c.retry_timer) {
69+
connect_websocket(port)
70+
}
4771
}
4872
}
4973

50-
export const connect_websocket = async (): Promise<void> => {
51-
if (
52-
is_reconnecting ||
53-
websocket?.readyState === WebSocket.OPEN ||
54-
websocket?.readyState === WebSocket.CONNECTING
55-
) {
56-
return
57-
}
74+
export const connect_websocket = async (port: number): Promise<void> => {
75+
const c = get_connection(port)
76+
if (is_busy(c)) return
5877

59-
is_reconnecting = true
60-
last_ping_timestamp = Date.now()
78+
if (c.retry_timer) {
79+
clearTimeout(c.retry_timer)
80+
c.retry_timer = null
81+
}
6182

6283
try {
63-
const is_healthy = await check_server_health()
64-
if (!is_healthy) {
65-
console.debug('Server health check failed, retrying in 5 seconds...')
66-
is_reconnecting = false
67-
setTimeout(check_and_recover_connection, 5000)
68-
return
69-
}
70-
7184
const manifest = browser.runtime.getManifest()
7285
const version = manifest.version
7386
const user_agent = navigator.userAgent
@@ -86,61 +99,99 @@ export const connect_websocket = async (): Promise<void> => {
8699
await browser.storage.local.set({ 'browser-id': browser_id })
87100
}
88101

89-
let ws_url = `ws://localhost:${DEFAULT_PORT}?client_type=${CLIENT_TYPE.BROWSER}&version=${version}&user_agent=${encodeURIComponent(user_agent)}&browser_id=${encodeURIComponent(browser_id)}`
102+
if (is_busy(c)) return
103+
104+
let ws_url = `ws://localhost:${port}?client_type=${CLIENT_TYPE.BROWSER}&version=${version}&user_agent=${encodeURIComponent(user_agent)}&browser_id=${encodeURIComponent(browser_id)}`
90105
if (profile_name) {
91106
ws_url += `&profile_name=${encodeURIComponent(profile_name)}`
92107
}
93108

94109
const ws = new WebSocket(ws_url)
95-
websocket = ws
110+
c.ws = ws
111+
c.last_ping = Date.now()
112+
let was_opened = false
96113

97114
ws.onopen = () => {
98-
if (websocket !== ws) return
99-
console.log('Connected with the VS Code!')
100-
is_reconnecting = false
101-
last_ping_timestamp = Date.now()
115+
if (c.ws !== ws) return
116+
was_opened = true
117+
c.last_ping = Date.now()
118+
console.log(`Connected with the VS Code on port ${port}!`)
119+
check_and_recover_connection()
102120
}
103121

104-
ws.onmessage = async (event) => {
105-
if (websocket !== ws) return
122+
ws.onmessage = (event) => {
123+
if (c.ws !== ws) return
106124
const message = JSON.parse(event.data)
107125
if (message.action === 'ping') {
108-
last_ping_timestamp = Date.now()
126+
c.last_ping = Date.now()
109127
if (ws.readyState === WebSocket.OPEN) {
110128
ws.send(JSON.stringify({ action: 'pong' }))
111129
}
112130
return
113131
}
132+
if (message.action === 'initialize-chat' && message.client_id) {
133+
client_id_to_port.set(message.client_id, port)
134+
}
114135
handle_messages(message as WebSocketMessage)
115136
}
116137

117138
ws.onclose = () => {
118-
console.log('Disconnected from VS Code, attempting to reconnect...')
119-
if (websocket === ws) {
120-
websocket = null
139+
if (c.ws !== ws) return
140+
c.ws = null
141+
if (was_opened) {
142+
console.log(
143+
`Disconnected from VS Code on port ${port}, attempting to reconnect...`
144+
)
121145
}
122-
is_reconnecting = false
123-
setTimeout(check_and_recover_connection, 1000)
146+
schedule_retry(port, was_opened ? RETRY_AFTER_DROP_MS : RETRY_IDLE_MS)
124147
}
148+
} catch {
149+
c.ws = null
150+
schedule_retry(port, RETRY_IDLE_MS)
151+
}
152+
}
125153

126-
ws.onerror = () => {
127-
if (websocket === ws) {
128-
websocket = null
129-
}
130-
is_reconnecting = false
131-
setTimeout(check_and_recover_connection, 1000)
154+
const reset_all_connections = () => {
155+
for (const port of ports_to_check()) {
156+
const c = get_connection(port)
157+
if (c.retry_timer) {
158+
clearTimeout(c.retry_timer)
159+
c.retry_timer = null
132160
}
133-
} catch {
134-
is_reconnecting = false
135-
setTimeout(check_and_recover_connection, 5000)
161+
const old_ws = c.ws
162+
c.ws = null
163+
old_ws?.close()
164+
connect_websocket(port)
136165
}
137166
}
138167

168+
browser.storage.onChanged.addListener((changes, areaName) => {
169+
if (areaName === 'local' && changes['browser-profile-name']) {
170+
reset_all_connections()
171+
}
172+
})
173+
139174
export const send_message_to_server = (message: any): boolean => {
140-
if (websocket?.readyState === WebSocket.OPEN) {
141-
websocket.send(JSON.stringify(message))
142-
return true
175+
const json = JSON.stringify(message)
176+
const port: number | undefined = message?.client_id
177+
? client_id_to_port.get(message.client_id)
178+
: undefined
179+
180+
if (port === undefined && message.action === 'apply-response') {
181+
console.warn('Dropping apply-response message: unknown client_id')
182+
return false
183+
}
184+
185+
const targets =
186+
port !== undefined ? [get_connection(port)] : [...connections.values()]
187+
188+
let sent = false
189+
for (const c of targets) {
190+
if (c.ws?.readyState === WebSocket.OPEN) {
191+
c.ws.send(json)
192+
sent = true
193+
}
143194
}
144-
console.warn('WebSocket not connected, cannot send message')
145-
return false
195+
if (!sent) console.warn('WebSocket not connected, cannot send message')
196+
return sent
146197
}

‎apps/browser/src/manifest.json‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"name": "Autofill for Code Web Chat",
55
"short_name": "CWC",
66
"description": "Autofill prompts in chatbots",
7-
"version": "2026.1021.21",
7+
"version": "2026.1025.22",
88
"homepage_url": "https://github.com/robertpiosik/CodeWebChat",
99
"icons": {
1010
"16": "icons/icon-16.png",
@@ -13,7 +13,6 @@
1313
"128": "icons/icon-128.png"
1414
},
1515
"permissions": ["storage", "alarms"],
16-
"host_permissions": ["http://localhost:55155/", "ws://localhost:55155/"],
1716
"action": {
1817
"default_title": "Autofill for Code Web Chat",
1918
"default_popup": "popup.html"

‎apps/editor/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "gemini-coder",
33
"displayName": "Code Web Chat",
44
"description": "CWC makes AI coding blazing fast!",
5-
"version": "2026.1024.292",
5+
"version": "2026.1025.293",
66
"scripts": {
77
"build": "npx vsce package --no-dependencies",
88
"vscode:prepublish": "rimraf out && npm run compile",

0 commit comments

Comments
 (0)