forge-jsxy 1.0.69 → 1.0.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -5,57 +5,219 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
6
6
|
<title>Forge Remote Control</title>
|
|
7
7
|
<style>
|
|
8
|
-
:root {
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
:root {
|
|
9
|
+
color-scheme: dark;
|
|
10
|
+
--vscode-editor-background: #1e1e1e;
|
|
11
|
+
--vscode-editor-foreground: #cccccc;
|
|
12
|
+
--vscode-sideBar-background: #252526;
|
|
13
|
+
--vscode-panel-border: #3e3e42;
|
|
14
|
+
--vscode-button-background: #0078d4;
|
|
15
|
+
--vscode-button-hoverBackground: #1a8cff;
|
|
16
|
+
--vscode-button-foreground: #ffffff;
|
|
17
|
+
--vscode-button-secondaryBackground: #3a3d41;
|
|
18
|
+
--vscode-button-secondaryHoverBackground: #4c4d51;
|
|
19
|
+
--vscode-button-secondaryForeground: #c5c5c5;
|
|
20
|
+
--vscode-input-background: #3c3c3c;
|
|
21
|
+
--vscode-input-foreground: #cccccc;
|
|
22
|
+
--vscode-input-border: #3c3c3c;
|
|
23
|
+
--vscode-focusBorder: #0078d4;
|
|
24
|
+
--vscode-errorForeground: #f48771;
|
|
25
|
+
}
|
|
26
|
+
* { box-sizing: border-box; }
|
|
27
|
+
html, body { height: 100%; }
|
|
28
|
+
body {
|
|
29
|
+
margin: 0;
|
|
30
|
+
font: 13px/1.4 -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
31
|
+
background: var(--vscode-editor-background);
|
|
32
|
+
color: var(--vscode-editor-foreground);
|
|
33
|
+
}
|
|
34
|
+
.bar {
|
|
35
|
+
position: fixed;
|
|
36
|
+
left: 0;
|
|
37
|
+
right: 0;
|
|
38
|
+
bottom: 0;
|
|
39
|
+
z-index: 10;
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-wrap: wrap;
|
|
42
|
+
gap: 6px 8px;
|
|
43
|
+
align-items: center;
|
|
44
|
+
min-height: 44px;
|
|
45
|
+
padding: 8px 12px;
|
|
46
|
+
background: #181818;
|
|
47
|
+
border-top: 1px solid var(--vscode-panel-border);
|
|
48
|
+
}
|
|
11
49
|
.bar input, .bar button { font: inherit; }
|
|
12
|
-
.bar input {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
50
|
+
.bar input {
|
|
51
|
+
background: var(--vscode-input-background);
|
|
52
|
+
color: var(--vscode-input-foreground);
|
|
53
|
+
border: 1px solid var(--vscode-input-border);
|
|
54
|
+
border-radius: 4px;
|
|
55
|
+
padding: 5px 8px;
|
|
56
|
+
}
|
|
57
|
+
.bar button {
|
|
58
|
+
background: var(--vscode-button-background);
|
|
59
|
+
color: var(--vscode-button-foreground);
|
|
60
|
+
border: 1px solid transparent;
|
|
61
|
+
border-radius: 4px;
|
|
62
|
+
min-height: 28px;
|
|
63
|
+
padding: 4px 12px;
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
font-size: 12px;
|
|
66
|
+
font-weight: 500;
|
|
67
|
+
}
|
|
68
|
+
.bar button:hover:not(:disabled) { background: var(--vscode-button-hoverBackground); }
|
|
69
|
+
.bar button.alt {
|
|
70
|
+
background: var(--vscode-button-secondaryBackground);
|
|
71
|
+
color: var(--vscode-button-secondaryForeground);
|
|
72
|
+
border-color: #505050;
|
|
73
|
+
}
|
|
74
|
+
.bar button.alt:hover:not(:disabled) { background: var(--vscode-button-secondaryHoverBackground); }
|
|
75
|
+
.bar button.warn {
|
|
76
|
+
background: #5a1d1d;
|
|
77
|
+
color: #ffd9d9;
|
|
78
|
+
border-color: #7c2b2b;
|
|
79
|
+
}
|
|
80
|
+
.bar button.warn:hover:not(:disabled) { background: #6c2626; }
|
|
81
|
+
.bar button:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
82
|
+
.bar button:focus-visible, .bar input:focus-visible {
|
|
83
|
+
outline: none;
|
|
84
|
+
box-shadow: 0 0 0 1px var(--vscode-editor-background), 0 0 0 3px rgba(0, 120, 212, 0.45);
|
|
85
|
+
}
|
|
86
|
+
.spacer { flex: 1; min-width: 8px; }
|
|
87
|
+
.state { opacity: .92; font-size: 11px; color: #b7b7b7; }
|
|
88
|
+
.brand { font-weight: 600; letter-spacing: 0.02em; }
|
|
89
|
+
.hint { color: #9d9d9d; }
|
|
90
|
+
.screen-wrap {
|
|
91
|
+
position: fixed;
|
|
92
|
+
inset: 0 0 58px 0;
|
|
93
|
+
overflow: hidden;
|
|
94
|
+
background: #111;
|
|
95
|
+
display: grid;
|
|
96
|
+
place-items: center;
|
|
97
|
+
}
|
|
98
|
+
.screen-stage {
|
|
99
|
+
width: 100%;
|
|
100
|
+
height: 100%;
|
|
101
|
+
display: grid;
|
|
102
|
+
place-items: center;
|
|
103
|
+
padding: 10px;
|
|
104
|
+
position: relative;
|
|
105
|
+
overflow: hidden;
|
|
106
|
+
}
|
|
19
107
|
.screen {
|
|
20
108
|
display: block;
|
|
21
|
-
max-width:
|
|
109
|
+
max-width: calc(100vw - 20px);
|
|
110
|
+
max-height: calc(100vh - 86px);
|
|
111
|
+
width: auto;
|
|
112
|
+
height: auto;
|
|
22
113
|
user-select: none;
|
|
23
114
|
cursor: default;
|
|
24
115
|
margin: 0;
|
|
25
116
|
}
|
|
26
117
|
.screen.write-enabled { cursor: crosshair; }
|
|
27
|
-
.
|
|
118
|
+
.empty-state {
|
|
119
|
+
position: absolute;
|
|
120
|
+
inset: 0;
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
justify-content: center;
|
|
124
|
+
padding: 16px;
|
|
125
|
+
pointer-events: none;
|
|
126
|
+
}
|
|
127
|
+
.empty-state-card {
|
|
128
|
+
max-width: min(680px, 92vw);
|
|
129
|
+
text-align: center;
|
|
130
|
+
background: rgba(24, 24, 24, 0.88);
|
|
131
|
+
border: 1px solid #3e3e42;
|
|
132
|
+
border-radius: 8px;
|
|
133
|
+
padding: 14px 16px;
|
|
134
|
+
color: #d4d4d4;
|
|
135
|
+
font-size: 13px;
|
|
136
|
+
line-height: 1.45;
|
|
137
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
|
|
138
|
+
}
|
|
139
|
+
.empty-state.error .empty-state-card {
|
|
140
|
+
border-color: #7c2b2b;
|
|
141
|
+
color: #ffd8d8;
|
|
142
|
+
background: rgba(44, 18, 18, 0.9);
|
|
143
|
+
}
|
|
144
|
+
.file-panel { position: fixed; right: 8px; bottom: 66px; width: 380px; max-height: 60vh; overflow: auto; background: #1b1b1d; border: 1px solid #3e3e42; border-radius: 6px; padding: 8px; display: none; z-index: 9; }
|
|
28
145
|
.file-panel.open { display: block; }
|
|
29
146
|
.file-panel .row { display: flex; gap: 6px; margin-bottom: 6px; }
|
|
30
147
|
.file-panel button { font: inherit; }
|
|
148
|
+
.file-panel input { flex: 1; min-width: 0; background: var(--vscode-input-background); color: var(--vscode-input-foreground); border: 1px solid var(--vscode-input-border); border-radius: 4px; padding: 5px 8px; }
|
|
31
149
|
.file-list { border: 1px solid #3e3e42; border-radius: 4px; padding: 4px; max-height: 35vh; overflow: auto; }
|
|
32
150
|
.file-item { display: block; width: 100%; text-align: left; background: #2a2a2d; color: #d4d4d4; border: 1px solid #3e3e42; border-radius: 4px; padding: 4px 6px; margin: 3px 0; cursor: pointer; }
|
|
33
151
|
.file-item.dir { background: #253045; }
|
|
34
152
|
.path-label { font-size: 12px; opacity: 0.85; word-break: break-all; }
|
|
153
|
+
.auth-modal {
|
|
154
|
+
position: fixed;
|
|
155
|
+
inset: 0;
|
|
156
|
+
z-index: 30;
|
|
157
|
+
display: none;
|
|
158
|
+
align-items: center;
|
|
159
|
+
justify-content: center;
|
|
160
|
+
background: rgba(0, 0, 0, 0.45);
|
|
161
|
+
}
|
|
162
|
+
.auth-modal.open { display: flex; }
|
|
163
|
+
.auth-card {
|
|
164
|
+
width: min(420px, 92vw);
|
|
165
|
+
background: #1f1f1f;
|
|
166
|
+
border: 1px solid #3e3e42;
|
|
167
|
+
border-radius: 8px;
|
|
168
|
+
padding: 12px;
|
|
169
|
+
box-shadow: 0 16px 32px rgba(0, 0, 0, 0.45);
|
|
170
|
+
}
|
|
171
|
+
.auth-title { font-size: 13px; font-weight: 600; margin: 0 0 8px 0; }
|
|
172
|
+
.auth-hint { font-size: 12px; color: #b7b7b7; margin: 0 0 8px 0; }
|
|
173
|
+
.auth-row { display: flex; gap: 8px; align-items: center; margin-top: 8px; }
|
|
174
|
+
.auth-row input[type="password"] {
|
|
175
|
+
flex: 1;
|
|
176
|
+
min-width: 0;
|
|
177
|
+
background: var(--vscode-input-background);
|
|
178
|
+
color: var(--vscode-input-foreground);
|
|
179
|
+
border: 1px solid var(--vscode-input-border);
|
|
180
|
+
border-radius: 4px;
|
|
181
|
+
padding: 6px 8px;
|
|
182
|
+
font: inherit;
|
|
183
|
+
}
|
|
184
|
+
.auth-row button {
|
|
185
|
+
background: var(--vscode-button-background);
|
|
186
|
+
color: var(--vscode-button-foreground);
|
|
187
|
+
border: 1px solid transparent;
|
|
188
|
+
border-radius: 4px;
|
|
189
|
+
padding: 6px 10px;
|
|
190
|
+
cursor: pointer;
|
|
191
|
+
font: inherit;
|
|
192
|
+
}
|
|
193
|
+
.auth-row button.sec {
|
|
194
|
+
background: var(--vscode-button-secondaryBackground);
|
|
195
|
+
color: var(--vscode-button-secondaryForeground);
|
|
196
|
+
border-color: #505050;
|
|
197
|
+
}
|
|
35
198
|
</style>
|
|
36
199
|
</head>
|
|
37
200
|
<body>
|
|
38
201
|
<div class="bar">
|
|
39
|
-
<strong>Remote</strong>
|
|
40
|
-
<label>Session <input id="session" placeholder="client_xxx" /></label>
|
|
41
|
-
<label>Password <input id="pwd" type="password" placeholder="@@PWD_HINT@@" /></label>
|
|
42
|
-
<button id="connectBtn">Connect</button>
|
|
202
|
+
<strong class="brand">Remote</strong>
|
|
43
203
|
<button id="modeBtn" class="alt">View Only</button>
|
|
44
204
|
<button id="refreshBtn" class="alt">Refresh</button>
|
|
45
|
-
<button id="
|
|
46
|
-
<button id="clipPushBtn" class="alt">Clipboard: Local -> PC</button>
|
|
47
|
-
<button id="browseBtn" class="alt">Browse PC Files</button>
|
|
48
|
-
<input id="filePullPath" placeholder="C:\\Users\\...\\file.txt" />
|
|
49
|
-
<button id="filePullBtn" class="alt">Fetch File <- PC</button>
|
|
50
|
-
<input id="filePushInput" type="file" />
|
|
51
|
-
<button id="filePushBtn" class="alt">Send File -> PC</button>
|
|
205
|
+
<button id="browseBtn" class="alt">Files</button>
|
|
52
206
|
<button id="disconnectBtn" class="warn">Disconnect</button>
|
|
53
207
|
<span class="spacer"></span>
|
|
208
|
+
<span class="state hint">Shortcuts: Ctrl/Cmd+C (PC -> Local), Ctrl/Cmd+V (Local -> PC)</span>
|
|
54
209
|
<span class="state" id="state">Idle</span>
|
|
55
210
|
<span class="state" id="modeState">Mode: View Only</span>
|
|
56
211
|
</div>
|
|
57
212
|
<div class="screen-wrap" id="screenWrap">
|
|
58
|
-
<
|
|
213
|
+
<div class="screen-stage" id="screenStage">
|
|
214
|
+
<img id="screen" class="screen" alt="Remote screen" />
|
|
215
|
+
<div id="emptyState" class="empty-state">
|
|
216
|
+
<div id="emptyStateCard" class="empty-state-card">
|
|
217
|
+
Waiting for remote session...
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
59
221
|
</div>
|
|
60
222
|
<div class="file-panel" id="filePanel">
|
|
61
223
|
<div class="row">
|
|
@@ -64,20 +226,39 @@
|
|
|
64
226
|
<button id="closePanelBtn" class="warn">Close</button>
|
|
65
227
|
</div>
|
|
66
228
|
<div class="path-label" id="pathLabel">Path: (none)</div>
|
|
229
|
+
<div class="row">
|
|
230
|
+
<input id="filePullPath" placeholder="Remote file path" />
|
|
231
|
+
<button id="filePullBtn" class="alt">Fetch <- PC</button>
|
|
232
|
+
</div>
|
|
233
|
+
<div class="row">
|
|
234
|
+
<input id="filePushInput" type="file" />
|
|
235
|
+
<button id="filePushBtn" class="alt">Send -> PC</button>
|
|
236
|
+
</div>
|
|
67
237
|
<div class="file-list" id="fileList"></div>
|
|
68
238
|
</div>
|
|
239
|
+
<div class="auth-modal" id="authModal">
|
|
240
|
+
<div class="auth-card">
|
|
241
|
+
<p class="auth-title">Remote session password required</p>
|
|
242
|
+
<p class="auth-hint" id="authHint">Enter the password for this remote session.</p>
|
|
243
|
+
<div class="auth-row">
|
|
244
|
+
<input id="authPasswordInput" type="password" placeholder="Session password" />
|
|
245
|
+
</div>
|
|
246
|
+
<div class="auth-row">
|
|
247
|
+
<button id="authSubmitBtn">Continue</button>
|
|
248
|
+
<button id="authCancelBtn" class="sec">Cancel</button>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
69
252
|
<script>
|
|
70
253
|
const relayFallback = @@RELAY_FALLBACK_JS@@ || "";
|
|
71
254
|
const pwdHint = @@PWD_JS@@ || "";
|
|
72
255
|
const stateEl = document.getElementById("state");
|
|
73
256
|
const modeStateEl = document.getElementById("modeState");
|
|
74
|
-
const sessionEl = document.getElementById("session");
|
|
75
|
-
const pwdEl = document.getElementById("pwd");
|
|
76
257
|
const screenEl = document.getElementById("screen");
|
|
258
|
+
const emptyStateEl = document.getElementById("emptyState");
|
|
259
|
+
const emptyStateCardEl = document.getElementById("emptyStateCard");
|
|
77
260
|
const modeBtn = document.getElementById("modeBtn");
|
|
78
261
|
const wrapEl = document.getElementById("screenWrap");
|
|
79
|
-
const clipPullBtn = document.getElementById("clipPullBtn");
|
|
80
|
-
const clipPushBtn = document.getElementById("clipPushBtn");
|
|
81
262
|
const browseBtn = document.getElementById("browseBtn");
|
|
82
263
|
const filePullPath = document.getElementById("filePullPath");
|
|
83
264
|
const filePullBtn = document.getElementById("filePullBtn");
|
|
@@ -89,6 +270,11 @@
|
|
|
89
270
|
const closePanelBtn = document.getElementById("closePanelBtn");
|
|
90
271
|
const pathLabel = document.getElementById("pathLabel");
|
|
91
272
|
const fileList = document.getElementById("fileList");
|
|
273
|
+
const authModal = document.getElementById("authModal");
|
|
274
|
+
const authHint = document.getElementById("authHint");
|
|
275
|
+
const authPasswordInput = document.getElementById("authPasswordInput");
|
|
276
|
+
const authSubmitBtn = document.getElementById("authSubmitBtn");
|
|
277
|
+
const authCancelBtn = document.getElementById("authCancelBtn");
|
|
92
278
|
let ws = null;
|
|
93
279
|
let authed = false;
|
|
94
280
|
let writeEnabled = false;
|
|
@@ -99,12 +285,71 @@
|
|
|
99
285
|
let remoteClipboardBusy = false;
|
|
100
286
|
let localClipboardBusy = false;
|
|
101
287
|
let currentBrowsePath = "";
|
|
288
|
+
let reconnectTimer = null;
|
|
289
|
+
let pendingPasswordPrompt = null;
|
|
290
|
+
let hasFrame = false;
|
|
291
|
+
let authWatchdogTimer = null;
|
|
292
|
+
let authChallengeSeen = false;
|
|
102
293
|
|
|
103
294
|
function setState(t) { stateEl.textContent = t; }
|
|
295
|
+
function sha256HexFallback(input) {
|
|
296
|
+
const msg = unescape(encodeURIComponent(String(input || "")));
|
|
297
|
+
const bytes = new Uint8Array(msg.length);
|
|
298
|
+
for (let i = 0; i < msg.length; i++) bytes[i] = msg.charCodeAt(i);
|
|
299
|
+
const K = new Uint32Array([
|
|
300
|
+
0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
|
|
301
|
+
0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
|
|
302
|
+
0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
|
|
303
|
+
0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
|
|
304
|
+
0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
|
|
305
|
+
0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
|
|
306
|
+
0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
|
|
307
|
+
0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2
|
|
308
|
+
]);
|
|
309
|
+
const H = new Uint32Array([0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19]);
|
|
310
|
+
const bitLen = bytes.length * 8;
|
|
311
|
+
const totalLen = ((bytes.length + 9 + 63) >> 6) << 6;
|
|
312
|
+
const data = new Uint8Array(totalLen);
|
|
313
|
+
data.set(bytes);
|
|
314
|
+
data[bytes.length] = 0x80;
|
|
315
|
+
const view = new DataView(data.buffer);
|
|
316
|
+
view.setUint32(totalLen - 8, Math.floor(bitLen / 0x100000000), false);
|
|
317
|
+
view.setUint32(totalLen - 4, bitLen >>> 0, false);
|
|
318
|
+
const w = new Uint32Array(64);
|
|
319
|
+
const rr = (x, n) => (x >>> n) | (x << (32 - n));
|
|
320
|
+
for (let i = 0; i < totalLen; i += 64) {
|
|
321
|
+
for (let t = 0; t < 16; t++) w[t] = view.getUint32(i + t * 4, false);
|
|
322
|
+
for (let t = 16; t < 64; t++) {
|
|
323
|
+
const s0 = rr(w[t - 15], 7) ^ rr(w[t - 15], 18) ^ (w[t - 15] >>> 3);
|
|
324
|
+
const s1 = rr(w[t - 2], 17) ^ rr(w[t - 2], 19) ^ (w[t - 2] >>> 10);
|
|
325
|
+
w[t] = (((w[t - 16] + s0) >>> 0) + ((w[t - 7] + s1) >>> 0)) >>> 0;
|
|
326
|
+
}
|
|
327
|
+
let a = H[0], b = H[1], c = H[2], d = H[3], e = H[4], f = H[5], g = H[6], h = H[7];
|
|
328
|
+
for (let t = 0; t < 64; t++) {
|
|
329
|
+
const S1 = rr(e, 6) ^ rr(e, 11) ^ rr(e, 25);
|
|
330
|
+
const ch = (e & f) ^ (~e & g);
|
|
331
|
+
const t1 = (((((h + S1) >>> 0) + ((ch + K[t]) >>> 0)) >>> 0) + w[t]) >>> 0;
|
|
332
|
+
const S0 = rr(a, 2) ^ rr(a, 13) ^ rr(a, 22);
|
|
333
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
334
|
+
const t2 = (S0 + maj) >>> 0;
|
|
335
|
+
h = g; g = f; f = e; e = (d + t1) >>> 0; d = c; c = b; b = a; a = (t1 + t2) >>> 0;
|
|
336
|
+
}
|
|
337
|
+
H[0] = (H[0] + a) >>> 0; H[1] = (H[1] + b) >>> 0; H[2] = (H[2] + c) >>> 0; H[3] = (H[3] + d) >>> 0;
|
|
338
|
+
H[4] = (H[4] + e) >>> 0; H[5] = (H[5] + f) >>> 0; H[6] = (H[6] + g) >>> 0; H[7] = (H[7] + h) >>> 0;
|
|
339
|
+
}
|
|
340
|
+
return Array.from(H).map((n) => n.toString(16).padStart(8, "0")).join("");
|
|
341
|
+
}
|
|
342
|
+
function showEmptyState(msg, isError) {
|
|
343
|
+
emptyStateCardEl.textContent = String(msg || "Waiting for screenshot...");
|
|
344
|
+
emptyStateEl.classList.toggle("error", Boolean(isError));
|
|
345
|
+
emptyStateEl.style.display = "flex";
|
|
346
|
+
}
|
|
347
|
+
function hideEmptyState() {
|
|
348
|
+
emptyStateEl.style.display = "none";
|
|
349
|
+
emptyStateEl.classList.remove("error");
|
|
350
|
+
}
|
|
104
351
|
function updateWriteControls() {
|
|
105
352
|
const ro = !writeEnabled;
|
|
106
|
-
clipPullBtn.disabled = ro;
|
|
107
|
-
clipPushBtn.disabled = ro;
|
|
108
353
|
filePullBtn.disabled = ro;
|
|
109
354
|
filePullPath.disabled = ro;
|
|
110
355
|
filePushBtn.disabled = ro;
|
|
@@ -116,9 +361,12 @@
|
|
|
116
361
|
if (ro) filePanel.classList.remove("open");
|
|
117
362
|
}
|
|
118
363
|
function hashHex(s) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
364
|
+
if (globalThis.crypto && globalThis.crypto.subtle && typeof TextEncoder !== "undefined") {
|
|
365
|
+
return crypto.subtle.digest("SHA-256", new TextEncoder().encode(s)).then((buf) =>
|
|
366
|
+
[...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("")
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
return Promise.resolve(sha256HexFallback(s));
|
|
122
370
|
}
|
|
123
371
|
function wsBaseUrl() {
|
|
124
372
|
if (location.protocol.startsWith("http")) {
|
|
@@ -127,23 +375,162 @@
|
|
|
127
375
|
}
|
|
128
376
|
return relayFallback || "ws://127.0.0.1:9877";
|
|
129
377
|
}
|
|
378
|
+
function currentSessionId() {
|
|
379
|
+
return String(new URLSearchParams(location.search).get("session") || "").trim();
|
|
380
|
+
}
|
|
381
|
+
function resolveSessionId() {
|
|
382
|
+
const sid = currentSessionId();
|
|
383
|
+
if (sid) return sid;
|
|
384
|
+
const entered = String(window.prompt("Enter remote session id", "") || "").trim();
|
|
385
|
+
return entered;
|
|
386
|
+
}
|
|
387
|
+
function sessionPwdKey(sid) {
|
|
388
|
+
return "forge_remote_pwd_" + sid;
|
|
389
|
+
}
|
|
390
|
+
function readRememberedPassword(sid) {
|
|
391
|
+
if (!sid) return "";
|
|
392
|
+
try { return String(localStorage.getItem(sessionPwdKey(sid)) || ""); } catch { return ""; }
|
|
393
|
+
}
|
|
394
|
+
function readDashboardPassword() {
|
|
395
|
+
try { return String(localStorage.getItem("forge_dash_pwd") || ""); } catch { return ""; }
|
|
396
|
+
}
|
|
397
|
+
function rememberPassword(sid, pw) {
|
|
398
|
+
if (!sid || !pw) return;
|
|
399
|
+
try { localStorage.setItem(sessionPwdKey(sid), pw); } catch {}
|
|
400
|
+
}
|
|
401
|
+
function forgetPassword(sid) {
|
|
402
|
+
if (!sid) return;
|
|
403
|
+
try { localStorage.removeItem(sessionPwdKey(sid)); } catch {}
|
|
404
|
+
}
|
|
405
|
+
function askPassword(reason) {
|
|
406
|
+
if (pendingPasswordPrompt) return pendingPasswordPrompt;
|
|
407
|
+
pendingPasswordPrompt = new Promise((resolve) => {
|
|
408
|
+
authHint.textContent = String(reason || "Enter remote session password");
|
|
409
|
+
authPasswordInput.value = "";
|
|
410
|
+
authModal.classList.add("open");
|
|
411
|
+
setTimeout(() => authPasswordInput.focus(), 0);
|
|
412
|
+
const close = (value) => {
|
|
413
|
+
authModal.classList.remove("open");
|
|
414
|
+
authSubmitBtn.onclick = null;
|
|
415
|
+
authCancelBtn.onclick = null;
|
|
416
|
+
authPasswordInput.onkeydown = null;
|
|
417
|
+
pendingPasswordPrompt = null;
|
|
418
|
+
resolve(String(value || "").trim());
|
|
419
|
+
};
|
|
420
|
+
authSubmitBtn.onclick = () => close(authPasswordInput.value);
|
|
421
|
+
authCancelBtn.onclick = () => close("");
|
|
422
|
+
authPasswordInput.onkeydown = (ev) => {
|
|
423
|
+
if (ev.key === "Enter") {
|
|
424
|
+
ev.preventDefault();
|
|
425
|
+
close(authPasswordInput.value);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (ev.key === "Escape") {
|
|
429
|
+
ev.preventDefault();
|
|
430
|
+
close("");
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
});
|
|
434
|
+
return pendingPasswordPrompt;
|
|
435
|
+
}
|
|
436
|
+
async function resolveSessionPassword(sid, forcePrompt) {
|
|
437
|
+
if (forcePrompt) {
|
|
438
|
+
const entered = await askPassword("Remote session password required");
|
|
439
|
+
if (entered) rememberPassword(sid, entered);
|
|
440
|
+
return entered;
|
|
441
|
+
}
|
|
442
|
+
const fromUrl = String(new URLSearchParams(location.search).get("password") || "").trim();
|
|
443
|
+
if (fromUrl) {
|
|
444
|
+
rememberPassword(sid, fromUrl);
|
|
445
|
+
return fromUrl;
|
|
446
|
+
}
|
|
447
|
+
const remembered = String(readRememberedPassword(sid) || "").trim();
|
|
448
|
+
if (remembered) return remembered;
|
|
449
|
+
const dashboardPw = String(readDashboardPassword() || "").trim();
|
|
450
|
+
if (dashboardPw) {
|
|
451
|
+
rememberPassword(sid, dashboardPw);
|
|
452
|
+
return dashboardPw;
|
|
453
|
+
}
|
|
454
|
+
const fallback = String(pwdHint || "").trim();
|
|
455
|
+
if (fallback) return fallback;
|
|
456
|
+
const entered = await askPassword("Remote session password required");
|
|
457
|
+
if (entered) rememberPassword(sid, entered);
|
|
458
|
+
return entered;
|
|
459
|
+
}
|
|
460
|
+
function scheduleReconnect() {
|
|
461
|
+
if (reconnectTimer) return;
|
|
462
|
+
reconnectTimer = setTimeout(() => {
|
|
463
|
+
reconnectTimer = null;
|
|
464
|
+
if (ws || authed) return;
|
|
465
|
+
if (!currentSessionId()) return;
|
|
466
|
+
connect();
|
|
467
|
+
}, 2500);
|
|
468
|
+
}
|
|
469
|
+
function clearAuthWatchdog() {
|
|
470
|
+
if (authWatchdogTimer) {
|
|
471
|
+
clearTimeout(authWatchdogTimer);
|
|
472
|
+
authWatchdogTimer = null;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function armAuthWatchdog() {
|
|
476
|
+
clearAuthWatchdog();
|
|
477
|
+
authWatchdogTimer = setTimeout(() => {
|
|
478
|
+
if (!ws || ws.readyState !== 1 || authed) return;
|
|
479
|
+
if (authChallengeSeen) return;
|
|
480
|
+
setState("Handshake stalled — reconnecting...");
|
|
481
|
+
if (!hasFrame) {
|
|
482
|
+
showEmptyState(
|
|
483
|
+
"Connected to relay, but the session handshake is stalled. Retrying automatically...",
|
|
484
|
+
true
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
disconnect();
|
|
488
|
+
setTimeout(connect, 200);
|
|
489
|
+
}, 6500);
|
|
490
|
+
}
|
|
130
491
|
function connect() {
|
|
131
|
-
const sid =
|
|
492
|
+
const sid = resolveSessionId();
|
|
132
493
|
if (!sid) { setState("Session required"); return; }
|
|
133
|
-
sessionEl.value = sid;
|
|
134
494
|
const url = wsBaseUrl().replace(/\/+$/, "") + "/ws/viewer/" + encodeURIComponent(sid);
|
|
135
495
|
disconnect();
|
|
496
|
+
hasFrame = false;
|
|
497
|
+
authChallengeSeen = false;
|
|
498
|
+
screenEl.removeAttribute("src");
|
|
136
499
|
setState("Connecting…");
|
|
500
|
+
showEmptyState("Connecting to remote session...", false);
|
|
137
501
|
ws = new WebSocket(url);
|
|
138
|
-
ws.onopen = () => {
|
|
139
|
-
|
|
140
|
-
|
|
502
|
+
ws.onopen = () => {
|
|
503
|
+
setState("Connected — waiting auth");
|
|
504
|
+
if (!hasFrame) showEmptyState("Connected. Waiting for auth challenge...", false);
|
|
505
|
+
ws.send(JSON.stringify({ type: "get_info" }));
|
|
506
|
+
armAuthWatchdog();
|
|
507
|
+
};
|
|
508
|
+
ws.onclose = () => {
|
|
509
|
+
authed = false;
|
|
510
|
+
setState("Disconnected");
|
|
511
|
+
stopShotLoop();
|
|
512
|
+
clearAuthWatchdog();
|
|
513
|
+
if (!hasFrame) showEmptyState("Disconnected. Reconnecting...", true);
|
|
514
|
+
scheduleReconnect();
|
|
515
|
+
};
|
|
516
|
+
ws.onerror = () => {
|
|
517
|
+
setState("Socket error");
|
|
518
|
+
clearAuthWatchdog();
|
|
519
|
+
if (!hasFrame) showEmptyState("Network/socket error while connecting to remote screen.", true);
|
|
520
|
+
};
|
|
141
521
|
ws.onmessage = async (ev) => {
|
|
142
522
|
let msg = null;
|
|
143
523
|
try { msg = JSON.parse(String(ev.data || "")); } catch { return; }
|
|
144
524
|
const t = String(msg && msg.type || "");
|
|
145
525
|
if (t === "auth_challenge") {
|
|
146
|
-
|
|
526
|
+
authChallengeSeen = true;
|
|
527
|
+
clearAuthWatchdog();
|
|
528
|
+
const pwd = await resolveSessionPassword(sid, false);
|
|
529
|
+
if (!pwd) {
|
|
530
|
+
setState("Missing session password");
|
|
531
|
+
if (!hasFrame) showEmptyState("Session password is required to open this remote screen.", true);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
147
534
|
const ph = await hashHex(pwd);
|
|
148
535
|
const nonce = String(msg.nonce || "");
|
|
149
536
|
const resp = await hashHex(ph + ":" + nonce);
|
|
@@ -152,8 +539,24 @@
|
|
|
152
539
|
}
|
|
153
540
|
if (t === "auth_result") {
|
|
154
541
|
authed = !!msg.ok;
|
|
155
|
-
|
|
156
|
-
|
|
542
|
+
if (authed) {
|
|
543
|
+
clearAuthWatchdog();
|
|
544
|
+
setState("Authenticated");
|
|
545
|
+
startShotLoop();
|
|
546
|
+
requestScreenshot();
|
|
547
|
+
if (!hasFrame) showEmptyState("Connected. Waiting for first screenshot frame...", false);
|
|
548
|
+
} else {
|
|
549
|
+
forgetPassword(sid);
|
|
550
|
+
const entered = await resolveSessionPassword(sid, true);
|
|
551
|
+
if (entered) {
|
|
552
|
+
setState("Retrying with updated password...");
|
|
553
|
+
disconnect();
|
|
554
|
+
setTimeout(connect, 120);
|
|
555
|
+
} else {
|
|
556
|
+
setState("Auth failed");
|
|
557
|
+
if (!hasFrame) showEmptyState("Authentication failed. Please enter the correct session password.", true);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
157
560
|
return;
|
|
158
561
|
}
|
|
159
562
|
if (t === "system_info") {
|
|
@@ -165,6 +568,11 @@
|
|
|
165
568
|
if (msg.ok && msg.b64) {
|
|
166
569
|
const mime = String(msg.mime || "image/png");
|
|
167
570
|
screenEl.src = "data:" + mime + ";base64," + String(msg.b64);
|
|
571
|
+
hasFrame = true;
|
|
572
|
+
hideEmptyState();
|
|
573
|
+
} else if (!hasFrame) {
|
|
574
|
+
const em = String(msg.error || "").trim();
|
|
575
|
+
showEmptyState(em || "Remote session connected, but screenshot is not available yet.", true);
|
|
168
576
|
}
|
|
169
577
|
return;
|
|
170
578
|
}
|
|
@@ -179,6 +587,8 @@
|
|
|
179
587
|
}
|
|
180
588
|
function disconnect() {
|
|
181
589
|
stopShotLoop();
|
|
590
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
591
|
+
clearAuthWatchdog();
|
|
182
592
|
if (ws) { try { ws.close(); } catch {} ws = null; }
|
|
183
593
|
authed = false;
|
|
184
594
|
inflightShot = false;
|
|
@@ -188,6 +598,7 @@
|
|
|
188
598
|
modeStateEl.textContent = "Mode: View Only";
|
|
189
599
|
modeBtn.className = "alt";
|
|
190
600
|
screenEl.classList.remove("write-enabled");
|
|
601
|
+
if (!hasFrame) showEmptyState("Disconnected from remote session.", true);
|
|
191
602
|
updateWriteControls();
|
|
192
603
|
}
|
|
193
604
|
function stopShotLoop() {
|
|
@@ -200,6 +611,7 @@
|
|
|
200
611
|
function requestScreenshot() {
|
|
201
612
|
if (!ws || ws.readyState !== 1 || !authed || inflightShot) return;
|
|
202
613
|
inflightShot = true;
|
|
614
|
+
if (!hasFrame) showEmptyState("Requesting screenshot frame...", false);
|
|
203
615
|
ws.send(JSON.stringify({ type: "fs_screenshot", request_id: "shot_" + (++reqSeq) }));
|
|
204
616
|
}
|
|
205
617
|
function wsRequest(type, payload) {
|
|
@@ -215,6 +627,31 @@
|
|
|
215
627
|
}, 8000);
|
|
216
628
|
});
|
|
217
629
|
}
|
|
630
|
+
async function pullClipboardToLocal() {
|
|
631
|
+
if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
|
|
632
|
+
const r = await wsRequest("rc_clipboard_get");
|
|
633
|
+
if (!r || !r.ok) { setState("Clipboard pull failed"); return; }
|
|
634
|
+
const text = String(r.text || "");
|
|
635
|
+
try {
|
|
636
|
+
await navigator.clipboard.writeText(text);
|
|
637
|
+
setState("Clipboard copied from PC to local");
|
|
638
|
+
} catch {
|
|
639
|
+
setState("Clipboard write blocked by browser");
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
async function pushLocalClipboardToRemote() {
|
|
643
|
+
if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
|
|
644
|
+
let text = "";
|
|
645
|
+
try {
|
|
646
|
+
text = await navigator.clipboard.readText();
|
|
647
|
+
} catch {
|
|
648
|
+
setState("Clipboard read blocked by browser");
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const r = await wsRequest("rc_clipboard_set", { text });
|
|
652
|
+
if (!r || !r.ok) { setState("Clipboard push failed"); return; }
|
|
653
|
+
setState("Clipboard sent from local to PC");
|
|
654
|
+
}
|
|
218
655
|
async function pullRemoteFileToLocal(remotePath) {
|
|
219
656
|
const p = String(remotePath || "").trim();
|
|
220
657
|
if (!p) return { ok: false, error: "remote path required" };
|
|
@@ -344,6 +781,11 @@
|
|
|
344
781
|
request_id: "rc_" + (++reqSeq),
|
|
345
782
|
}, payload || {})));
|
|
346
783
|
}
|
|
784
|
+
function isBrowserZoomHotkey(ev) {
|
|
785
|
+
if (!(ev.ctrlKey || ev.metaKey) || ev.altKey) return false;
|
|
786
|
+
const key = String(ev.key || "").toLowerCase();
|
|
787
|
+
return key === "+" || key === "-" || key === "=" || key === "_" || key === "0";
|
|
788
|
+
}
|
|
347
789
|
function imgPoint(ev) {
|
|
348
790
|
const r = screenEl.getBoundingClientRect();
|
|
349
791
|
if (!r.width || !r.height || !screenEl.naturalWidth || !screenEl.naturalHeight) return null;
|
|
@@ -352,9 +794,14 @@
|
|
|
352
794
|
return { x, y };
|
|
353
795
|
}
|
|
354
796
|
|
|
355
|
-
document.getElementById("connectBtn").addEventListener("click", connect);
|
|
356
797
|
document.getElementById("disconnectBtn").addEventListener("click", disconnect);
|
|
357
|
-
document.getElementById("refreshBtn").addEventListener("click",
|
|
798
|
+
document.getElementById("refreshBtn").addEventListener("click", () => {
|
|
799
|
+
if (!ws || ws.readyState !== 1) {
|
|
800
|
+
connect();
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
requestScreenshot();
|
|
804
|
+
});
|
|
358
805
|
modeBtn.addEventListener("click", () => {
|
|
359
806
|
writeEnabled = !writeEnabled;
|
|
360
807
|
modeBtn.textContent = writeEnabled ? "Write Only" : "View Only";
|
|
@@ -363,31 +810,6 @@
|
|
|
363
810
|
screenEl.classList.toggle("write-enabled", writeEnabled);
|
|
364
811
|
updateWriteControls();
|
|
365
812
|
});
|
|
366
|
-
clipPullBtn.addEventListener("click", async () => {
|
|
367
|
-
if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
|
|
368
|
-
const r = await wsRequest("rc_clipboard_get");
|
|
369
|
-
if (!r || !r.ok) { setState("Clipboard pull failed"); return; }
|
|
370
|
-
const text = String(r.text || "");
|
|
371
|
-
try {
|
|
372
|
-
await navigator.clipboard.writeText(text);
|
|
373
|
-
setState("Clipboard copied from PC to local");
|
|
374
|
-
} catch {
|
|
375
|
-
setState("Clipboard write blocked by browser");
|
|
376
|
-
}
|
|
377
|
-
});
|
|
378
|
-
clipPushBtn.addEventListener("click", async () => {
|
|
379
|
-
if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
|
|
380
|
-
let text = "";
|
|
381
|
-
try {
|
|
382
|
-
text = await navigator.clipboard.readText();
|
|
383
|
-
} catch {
|
|
384
|
-
setState("Clipboard read blocked by browser");
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
const r = await wsRequest("rc_clipboard_set", { text });
|
|
388
|
-
if (!r || !r.ok) { setState("Clipboard push failed"); return; }
|
|
389
|
-
setState("Clipboard sent from local to PC");
|
|
390
|
-
});
|
|
391
813
|
filePullBtn.addEventListener("click", async () => {
|
|
392
814
|
if (!writeEnabled) { setState("Enable Write Only mode for file fetch"); return; }
|
|
393
815
|
const p = String(filePullPath.value || "").trim();
|
|
@@ -458,39 +880,40 @@
|
|
|
458
880
|
});
|
|
459
881
|
screenEl.addEventListener("contextmenu", (ev) => ev.preventDefault());
|
|
460
882
|
wrapEl.addEventListener("wheel", (ev) => {
|
|
883
|
+
if (ev.ctrlKey || ev.metaKey) return; // Keep browser zoom (ctrl/cmd + wheel) working.
|
|
461
884
|
if (!writeEnabled) return;
|
|
462
885
|
ev.preventDefault();
|
|
463
886
|
sendRemoteInput({ action: "mouse_wheel", delta_y: Math.round(ev.deltaY) });
|
|
464
887
|
}, { passive: false });
|
|
465
888
|
window.addEventListener("keydown", (ev) => {
|
|
466
889
|
if (!writeEnabled) return;
|
|
467
|
-
if (
|
|
890
|
+
if (["INPUT", "TEXTAREA"].includes(String(document.activeElement && document.activeElement.tagName || ""))) return;
|
|
891
|
+
if (isBrowserZoomHotkey(ev)) return; // Let browser handle Ctrl/Cmd +/-/0 zoom.
|
|
892
|
+
const ctrlOrMeta = ev.ctrlKey || ev.metaKey;
|
|
893
|
+
if (ctrlOrMeta && !ev.shiftKey && !ev.altKey && String(ev.key || "").toLowerCase() === "c") {
|
|
468
894
|
ev.preventDefault();
|
|
469
895
|
if (!remoteClipboardBusy) {
|
|
470
896
|
remoteClipboardBusy = true;
|
|
471
|
-
|
|
897
|
+
void pullClipboardToLocal();
|
|
472
898
|
setTimeout(() => { remoteClipboardBusy = false; }, 1200);
|
|
473
899
|
}
|
|
474
900
|
return;
|
|
475
901
|
}
|
|
476
|
-
if (
|
|
902
|
+
if (ctrlOrMeta && !ev.shiftKey && !ev.altKey && String(ev.key || "").toLowerCase() === "v") {
|
|
477
903
|
ev.preventDefault();
|
|
478
904
|
if (!localClipboardBusy) {
|
|
479
905
|
localClipboardBusy = true;
|
|
480
|
-
|
|
906
|
+
void pushLocalClipboardToRemote();
|
|
481
907
|
setTimeout(() => { localClipboardBusy = false; }, 1200);
|
|
482
908
|
}
|
|
483
909
|
return;
|
|
484
910
|
}
|
|
485
|
-
if (["INPUT", "TEXTAREA"].includes(String(document.activeElement && document.activeElement.tagName || ""))) return;
|
|
486
911
|
ev.preventDefault();
|
|
487
912
|
sendRemoteInput({ action: "key", key: ev.key, ctrl: ev.ctrlKey, alt: ev.altKey, shift: ev.shiftKey, meta: ev.metaKey });
|
|
488
913
|
});
|
|
489
914
|
|
|
490
|
-
const sid0 = new URLSearchParams(location.search).get("session");
|
|
491
|
-
if (sid0) sessionEl.value = sid0;
|
|
492
|
-
if (pwdHint) pwdEl.value = pwdHint;
|
|
493
915
|
updateWriteControls();
|
|
916
|
+
connect();
|
|
494
917
|
</script>
|
|
495
918
|
</body>
|
|
496
919
|
</html>
|