@langify-org/dsh-code-server 0.1.1 → 0.2.0
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.
- package/README.ja.md +12 -9
- package/README.md +12 -9
- package/README.zh.md +12 -9
- package/dist/client.cjs +20 -2
- package/dist/index.mjs +189 -101
- package/extension/dsh-code-bridge.vsix +0 -0
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -29,24 +29,26 @@ DSH の Web 版や Desktop 版では **Plugins** ページから追加できま
|
|
|
29
29
|
|
|
30
30
|
## 設定
|
|
31
31
|
|
|
32
|
-
| 設定 | 既定値 | 意味
|
|
33
|
-
| --------------- | ------------- |
|
|
34
|
-
| `command` | `code-server` | 実行ファイルの名前またはパス。
|
|
35
|
-
| `userDataDir` | 空 | code-server の `--user-data-dir`。空なら code-server の既定。
|
|
36
|
-
| `extensionsDir` | 空 | code-server の `--extensions-dir`。空なら code-server の既定。
|
|
32
|
+
| 設定 | 既定値 | 意味 |
|
|
33
|
+
| --------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
34
|
+
| `command` | `code-server` | 実行ファイルの名前またはパス。 |
|
|
35
|
+
| `userDataDir` | 空 | code-server の `--user-data-dir`。空なら code-server の既定。 |
|
|
36
|
+
| `extensionsDir` | 空 | code-server の `--extensions-dir`。空なら code-server の既定。 |
|
|
37
|
+
| `desktopPort` | `41820` | DSH Desktop がエディターを読み込む `127.0.0.1` のポート。固定のままにしてください。信頼済みフォルダーなどエディターの状態はオリジンごとに保存されます。 |
|
|
37
38
|
|
|
38
39
|
既定のままなら、普段使いの code-server と設定・拡張を共有します。同じディレクトリを使う別の code-server を同時に動かさないでください。
|
|
39
40
|
|
|
40
41
|
## 仕組み
|
|
41
42
|
|
|
42
|
-
- プラグインは必要になったときに DSH 用の code-server を1つ起動します。待ち受けは本人だけが開ける Unix socket(`0700` のディレクトリ内の `0600`)で、code-server
|
|
43
|
-
- DSH
|
|
43
|
+
- プラグインは必要になったときに DSH 用の code-server を1つ起動します。待ち受けは本人だけが開ける Unix socket(`0700` のディレクトリ内の `0600`)で、code-server 自身の認証は無効にしています。code-server 自体はネットワークのポートを開きません。
|
|
44
|
+
- ブラウザーでは、DSH が自分のオリジンの `/code-server` でエディターを配信します。すべてのリクエストと WebSocket は、先に DSH の認証とオリジンの検査を通ります。そのためエディターには、DSH に届く場所(Tailscale 経由など)からしか届きません。
|
|
45
|
+
- DSH Desktop は DSH を `dsh-app://app` で表示します。code-server はここでは動きません。サーバーの宛先を自分のページのアドレスから作り、このスキームでは webview に必要な Service Worker も使えないためです。また Desktop は、自分以外のオリジンから Host への WebSocket を遮断します。そこで Desktop では、プラグイン自身が `127.0.0.1:<desktopPort>` でエディターを配信します。DSH とは別の、ふつうの http のオリジンです。待ち受けは、Desktop が初めて Code ページを開いたときに始めます。エディターのアドレスのパスには、プラグインの起動ごとに作るランダムなトークンが入ります。code-server はすべてをページからの相対パスで指すので、どのリクエストにもトークンが付きます。トークンを受け取れるのは、DSH が認証したページだけです。さらに、別のホスト名を名乗るリクエストは拒否し、DNS rebinding を防ぎます。
|
|
44
46
|
- 起動のたびに、同梱の VS Code 拡張 **DSH Code Bridge**(`langify-org.dsh-code-bridge`)を code-server に入れ、古いものを置き換えます。
|
|
45
47
|
- Code ページごとに専用の接続(binding)を作ります。ページは binding をエディターの URL の断片(サーバーには送られない部分)に入れ、そのエディター画面の拡張がそれを読み取って、別の非公開 socket でプラグインに接続します。同じフォルダーを複数のページで開いていても、ファイルはその画面にだけ開きます。
|
|
46
48
|
|
|
47
49
|
## セキュリティ
|
|
48
50
|
|
|
49
|
-
code-server とそのターミナル・拡張は、あなたの権限で、DSH
|
|
51
|
+
code-server とそのターミナル・拡張は、あなたの権限で、DSH のサンドボックスや承認の外で動きます。ブラウザーではエディターが DSH と同じオリジンなので、code-server の中で動くコード(拡張やその webview を含む)は、あなたのセッションで DSH を操作することもできます。信頼できる拡張だけを入れてください。[SECURITY.ja.md](../../SECURITY.ja.md) も参照してください。
|
|
50
52
|
|
|
51
53
|
## 手動確認
|
|
52
54
|
|
|
@@ -56,7 +58,8 @@ code-server とそのターミナル・拡張は、あなたの権限で、DSH
|
|
|
56
58
|
2. **Workspace files** で選んだテキストファイルが Code ページの該当行で開く。画像は DSH のビューアーのまま開く。
|
|
57
59
|
3. サインインしていないと `/code-server/` と `POST /api/langify-code-server/create` が 401 を返し、別オリジンからの WebSocket の upgrade は 403 を返す。
|
|
58
60
|
4. セッションの作業フォルダーが変わると、エディターが開き直すかを尋ねる。未保存のファイルがあれば先に保存を求め、そのあとページが新しいフォルダーでエディターを開き直す。
|
|
59
|
-
5.
|
|
61
|
+
5. DSH Desktop では、エディターが `http://127.0.0.1:41820/…` から読み込まれ、テーマとファイルアイコンが出て、Markdown のプレビューが開く。トークンが無いとポートは 404 を返す。
|
|
62
|
+
6. Code タブを閉じると binding が閉じる。DSH を止めると code-server も止まり、socket が消える。
|
|
60
63
|
|
|
61
64
|
## 既知の制約
|
|
62
65
|
|
package/README.md
CHANGED
|
@@ -29,24 +29,26 @@ Open the right sidebar, add a tab, and choose **Code**. The page opens the sessi
|
|
|
29
29
|
|
|
30
30
|
## Configuration
|
|
31
31
|
|
|
32
|
-
| Setting | Default | Meaning
|
|
33
|
-
| --------------- | ------------- |
|
|
34
|
-
| `command` | `code-server` | Executable name or path.
|
|
35
|
-
| `userDataDir` | empty | code-server's `--user-data-dir`. Empty uses code-server's default.
|
|
36
|
-
| `extensionsDir` | empty | code-server's `--extensions-dir`. Empty uses code-server's default.
|
|
32
|
+
| Setting | Default | Meaning |
|
|
33
|
+
| --------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
34
|
+
| `command` | `code-server` | Executable name or path. |
|
|
35
|
+
| `userDataDir` | empty | code-server's `--user-data-dir`. Empty uses code-server's default. |
|
|
36
|
+
| `extensionsDir` | empty | code-server's `--extensions-dir`. Empty uses code-server's default. |
|
|
37
|
+
| `desktopPort` | `41820` | Port on `127.0.0.1` where DSH Desktop loads the editor. Keep it fixed: the editor keeps its state, such as trusted folders, per origin. |
|
|
37
38
|
|
|
38
39
|
With the defaults, the editor shares settings and extensions with your own code-server. Do not run another code-server on the same directories at the same time.
|
|
39
40
|
|
|
40
41
|
## How it works
|
|
41
42
|
|
|
42
|
-
- The plugin starts one code-server for DSH on demand, listening on a Unix socket that only you can open (mode `0600` in a `0700` directory), with code-server's own authentication off.
|
|
43
|
-
- DSH serves
|
|
43
|
+
- The plugin starts one code-server for DSH on demand, listening on a Unix socket that only you can open (mode `0600` in a `0700` directory), with code-server's own authentication off. code-server itself never opens a network port.
|
|
44
|
+
- In a browser, DSH serves the editor under `/code-server` on its own origin. Every request and WebSocket passes DSH's authentication and origin check first, so the editor is reachable wherever DSH is, for example over Tailscale, and nowhere else.
|
|
45
|
+
- DSH Desktop shows DSH at `dsh-app://app`, where code-server cannot run: it builds its server's addresses from its own page's, and that scheme allows no service workers, which webviews need. Desktop also refuses WebSockets to its Host from any other origin. So in Desktop the plugin serves the editor itself, on `127.0.0.1:<desktopPort>`, a plain http origin apart from DSH's. It starts listening when Desktop first opens the Code page. The editor's address carries in its path a random token, made anew whenever the plugin starts; code-server addresses everything relative to its page, so every request carries it. Only pages DSH has authenticated receive the token. The port also refuses requests that name another host, which stops DNS rebinding.
|
|
44
46
|
- Before each start, the plugin installs its bundled VS Code extension, **DSH Code Bridge** (`langify-org.dsh-code-bridge`), into code-server, replacing any older copy.
|
|
45
47
|
- Each Code page gets its own connection ("binding"). The page puts the binding in the editor's URL fragment, which never reaches a server; the extension in that editor window reads it back and connects to the plugin over another private socket. Files open in that window only, even when several pages show the same folder.
|
|
46
48
|
|
|
47
49
|
## Security
|
|
48
50
|
|
|
49
|
-
code-server runs as you, and so do its terminal and extensions, outside DSH's sandbox and approvals.
|
|
51
|
+
code-server runs as you, and so do its terminal and extensions, outside DSH's sandbox and approvals. In a browser the editor shares DSH's origin, so code running in code-server, including extensions and their webviews, can also use DSH with your session. In DSH Desktop the editor has its own origin, and other programs on this computer can reach its port but not without the token. Install only extensions you trust. See also [SECURITY.md](../../SECURITY.md).
|
|
50
52
|
|
|
51
53
|
## Manual checks
|
|
52
54
|
|
|
@@ -56,7 +58,8 @@ These need a browser and a real code-server, so they are not part of `pnpm check
|
|
|
56
58
|
2. A text file chosen in **Workspace files** opens in the Code page at its line; an image keeps DSH's viewer.
|
|
57
59
|
3. Without signing in, `/code-server/` and `POST /api/langify-code-server/create` answer 401, and a WebSocket upgrade from another origin answers 403.
|
|
58
60
|
4. After the session's working folder changes, the editor offers to reopen; with an unsaved file it asks to save first, then the page reloads the editor on the new folder.
|
|
59
|
-
5.
|
|
61
|
+
5. In DSH Desktop, the editor loads from `http://127.0.0.1:41820/…` with your theme and file icons, and a Markdown preview opens. The port answers 404 without the token.
|
|
62
|
+
6. Closing the Code tab closes its binding; stopping DSH stops code-server and removes its sockets.
|
|
60
63
|
|
|
61
64
|
## Known limitations
|
|
62
65
|
|
package/README.zh.md
CHANGED
|
@@ -29,24 +29,26 @@ dsh plugin --profile <profile> add @langify-org/dsh-code-server
|
|
|
29
29
|
|
|
30
30
|
## 配置
|
|
31
31
|
|
|
32
|
-
| 设置 | 默认值 | 含义
|
|
33
|
-
| --------------- | ------------- |
|
|
34
|
-
| `command` | `code-server` | 可执行文件的名称或路径。
|
|
35
|
-
| `userDataDir` | 空 | code-server 的 `--user-data-dir`。为空时使用其默认值。
|
|
36
|
-
| `extensionsDir` | 空 | code-server 的 `--extensions-dir`。为空时使用其默认值。
|
|
32
|
+
| 设置 | 默认值 | 含义 |
|
|
33
|
+
| --------------- | ------------- | ----------------------------------------------------------------------------------------------------- |
|
|
34
|
+
| `command` | `code-server` | 可执行文件的名称或路径。 |
|
|
35
|
+
| `userDataDir` | 空 | code-server 的 `--user-data-dir`。为空时使用其默认值。 |
|
|
36
|
+
| `extensionsDir` | 空 | code-server 的 `--extensions-dir`。为空时使用其默认值。 |
|
|
37
|
+
| `desktopPort` | `41820` | DSH Desktop 加载编辑器所用的 `127.0.0.1` 端口。请保持固定:编辑器的状态(如受信任的文件夹)按源保存。 |
|
|
37
38
|
|
|
38
39
|
使用默认值时,编辑器会与你平时使用的 code-server 共享设置和扩展。请不要同时运行使用相同目录的另一个 code-server。
|
|
39
40
|
|
|
40
41
|
## 工作原理
|
|
41
42
|
|
|
42
|
-
- 插件在需要时为 DSH 启动一个 code-server,监听只有你能打开的 Unix 套接字(位于 `0700` 目录中,权限为 `0600`),并关闭 code-server
|
|
43
|
-
- DSH 在自己的源下通过 `/code-server`
|
|
43
|
+
- 插件在需要时为 DSH 启动一个 code-server,监听只有你能打开的 Unix 套接字(位于 `0700` 目录中,权限为 `0600`),并关闭 code-server 自身的认证。code-server 本身不会打开任何网络端口。
|
|
44
|
+
- 在浏览器中,DSH 在自己的源下通过 `/code-server` 提供编辑器。每个请求和 WebSocket 都会先经过 DSH 的认证和来源检查,因此只有能访问 DSH 的地方(例如通过 Tailscale)才能访问编辑器。
|
|
45
|
+
- DSH Desktop 在 `dsh-app://app` 显示 DSH,code-server 无法在那里运行:它根据自身页面的地址构造服务器地址,而且该协议不支持 webview 所需的 Service Worker。Desktop 还会拒绝来自其他源、发往其 Host 的 WebSocket。因此在 Desktop 中,插件自己在 `127.0.0.1:<desktopPort>` 提供编辑器,这是一个独立于 DSH 的普通 http 源。Desktop 第一次打开 Code 页面时才开始监听。编辑器地址的路径中带有一个随机令牌,插件每次启动时重新生成;code-server 的所有地址都相对于其页面,因此每个请求都会带上它。只有经过 DSH 认证的页面才能拿到该令牌。该端口还会拒绝声明为其他主机名的请求,以防 DNS rebinding。
|
|
44
46
|
- 每次启动前,插件都会把内置的 VS Code 扩展 **DSH Code Bridge**(`langify-org.dsh-code-bridge`)安装到 code-server 中,替换旧版本。
|
|
45
47
|
- 每个 Code 页面都有自己的连接(binding)。页面把 binding 放在编辑器 URL 的片段中(这一部分不会发送到服务器),该编辑器窗口中的扩展读取它,并通过另一个私有套接字连接到插件。即使多个页面打开了同一个文件夹,文件也只会在对应的窗口中打开。
|
|
46
48
|
|
|
47
49
|
## 安全
|
|
48
50
|
|
|
49
|
-
code-server 及其终端和扩展以你的身份运行,不受 DSH
|
|
51
|
+
code-server 及其终端和扩展以你的身份运行,不受 DSH 沙箱和审批的约束。在浏览器中,编辑器与 DSH 同源,因此在 code-server 中运行的代码(包括扩展及其 webview)也能以你的会话使用 DSH。请只安装你信任的扩展。另请参阅 [SECURITY.zh.md](../../SECURITY.zh.md)。
|
|
50
52
|
|
|
51
53
|
## 手动检查
|
|
52
54
|
|
|
@@ -56,7 +58,8 @@ code-server 及其终端和扩展以你的身份运行,不受 DSH 沙箱和审
|
|
|
56
58
|
2. 在 **Workspace files** 中选择的文本文件会在 Code 页面的对应行打开;图片仍使用 DSH 的查看器。
|
|
57
59
|
3. 未登录时,`/code-server/` 和 `POST /api/langify-code-server/create` 返回 401,来自其他源的 WebSocket upgrade 返回 403。
|
|
58
60
|
4. 会话的工作文件夹变化后,编辑器会询问是否重新打开;如有未保存的文件,会先要求保存,然后页面在新文件夹中重新打开编辑器。
|
|
59
|
-
5.
|
|
61
|
+
5. 在 DSH Desktop 中,编辑器从 `http://127.0.0.1:41820/…` 加载,显示你的主题和文件图标,Markdown 预览可以打开;没有令牌时该端口返回 404。
|
|
62
|
+
6. 关闭 Code 标签页会关闭其 binding;停止 DSH 会停止 code-server 并删除其套接字。
|
|
60
63
|
|
|
61
64
|
## 已知限制
|
|
62
65
|
|
package/dist/client.cjs
CHANGED
|
@@ -35,6 +35,7 @@ window.__ModuleLoader__.load({
|
|
|
35
35
|
"error.different_host": "This session runs on another file system, which Code cannot open.",
|
|
36
36
|
"error.code_server_not_found": "code-server was not found. Install it, or set its command in the plugin settings.",
|
|
37
37
|
"error.code_server_failed": "code-server failed to start: {message}",
|
|
38
|
+
"error.desktop_port_in_use": "The editor's port for DSH Desktop is taken: {message}. Choose another one with the plugin's desktopPort setting.",
|
|
38
39
|
"error.unknown_binding": "This editor connection is gone. Reconnect.",
|
|
39
40
|
"error.not_ready": "The editor is still starting.",
|
|
40
41
|
"error.binding_disconnected": "The editor window disconnected. Reconnect.",
|
|
@@ -79,6 +80,7 @@ window.__ModuleLoader__.load({
|
|
|
79
80
|
"error.different_host": "此会话运行在其他文件系统上,Code 无法打开。",
|
|
80
81
|
"error.code_server_not_found": "未找到 code-server。请安装它,或在插件设置中指定其命令。",
|
|
81
82
|
"error.code_server_failed": "code-server 启动失败:{message}",
|
|
83
|
+
"error.desktop_port_in_use": "供 DSH Desktop 使用的编辑器端口已被占用:{message}。请在插件设置 desktopPort 中改用其他端口。",
|
|
82
84
|
"error.unknown_binding": "与此编辑器的连接已不存在。请重新连接。",
|
|
83
85
|
"error.not_ready": "编辑器仍在启动中。",
|
|
84
86
|
"error.binding_disconnected": "编辑器窗口已断开。请重新连接。",
|
|
@@ -123,6 +125,7 @@ window.__ModuleLoader__.load({
|
|
|
123
125
|
"error.different_host": "このセッションは別のファイルシステムで動いているため、Code では開けません。",
|
|
124
126
|
"error.code_server_not_found": "code-server が見つかりません。インストールするか、プラグインの設定でコマンドを指定してください。",
|
|
125
127
|
"error.code_server_failed": "code-server を起動できませんでした: {message}",
|
|
128
|
+
"error.desktop_port_in_use": "DSH Desktop 用のエディターのポートが使われています: {message}。プラグインの設定 desktopPort で別の番号にしてください。",
|
|
126
129
|
"error.unknown_binding": "このエディターとの接続はもうありません。再接続してください。",
|
|
127
130
|
"error.not_ready": "エディターはまだ起動中です。",
|
|
128
131
|
"error.binding_disconnected": "エディターの画面が切断されました。再接続してください。",
|
|
@@ -148,6 +151,12 @@ window.__ModuleLoader__.load({
|
|
|
148
151
|
/** DSH's shared RPC channel, and the prefix of the endpoints this plugin owns on it. */
|
|
149
152
|
const RPC_CHANNEL = "/api";
|
|
150
153
|
const RPC_PREFIX = "langify-code-server/";
|
|
154
|
+
/**
|
|
155
|
+
* Where the page loads the editor from. `dsh` serves it under DSH's own origin;
|
|
156
|
+
* `desktop` is the plugin's own address on 127.0.0.1, for DSH Desktop, whose
|
|
157
|
+
* page is not on http(s) (see host/ingress/).
|
|
158
|
+
*/
|
|
159
|
+
const INGRESSES = ["dsh", "desktop"];
|
|
151
160
|
const id = zod.z.string().min(1).max(256).refine((value) => !value.includes("\0"));
|
|
152
161
|
const position = zod.z.int().min(1).max(1e6);
|
|
153
162
|
const binding = {
|
|
@@ -156,7 +165,8 @@ window.__ModuleLoader__.load({
|
|
|
156
165
|
};
|
|
157
166
|
zod.z.strictObject({
|
|
158
167
|
sessionId: id,
|
|
159
|
-
ownerId: id
|
|
168
|
+
ownerId: id,
|
|
169
|
+
ingress: zod.z.enum(INGRESSES)
|
|
160
170
|
}), zod.z.strictObject({
|
|
161
171
|
...binding,
|
|
162
172
|
revision: zod.z.int().min(-1)
|
|
@@ -311,6 +321,11 @@ window.__ModuleLoader__.load({
|
|
|
311
321
|
}
|
|
312
322
|
// src/client/code-page.tsx
|
|
313
323
|
const SANDBOX = "allow-scripts allow-forms allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-downloads";
|
|
324
|
+
/**
|
|
325
|
+
* code-server runs only on an http(s) page. DSH Desktop's page is not one, so
|
|
326
|
+
* the editor comes from the plugin's own address instead (host/ingress/).
|
|
327
|
+
*/
|
|
328
|
+
const INGRESS = location.protocol === "http:" || location.protocol === "https:" ? "dsh" : "desktop";
|
|
314
329
|
/** After these, the binding is gone and the page must reconnect. */
|
|
315
330
|
const FINAL_ERRORS = /* @__PURE__ */ new Set([
|
|
316
331
|
"unknown_binding",
|
|
@@ -378,7 +393,10 @@ window.__ModuleLoader__.load({
|
|
|
378
393
|
};
|
|
379
394
|
(async () => {
|
|
380
395
|
try {
|
|
381
|
-
binding = await call("create", {
|
|
396
|
+
binding = await call("create", {
|
|
397
|
+
ownerId: crypto.randomUUID(),
|
|
398
|
+
ingress: INGRESS
|
|
399
|
+
});
|
|
382
400
|
if (disposed) {
|
|
383
401
|
call("close", { bindingId: binding.bindingId }).catch(() => {});
|
|
384
402
|
return;
|
package/dist/index.mjs
CHANGED
|
@@ -4,9 +4,9 @@ import { isAbsolute, join } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import Schema from "@deepseek-ai/schemastery";
|
|
6
6
|
import { z } from "zod";
|
|
7
|
-
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
8
8
|
import { createServer } from "node:net";
|
|
9
|
-
import { STATUS_CODES, get, request } from "node:http";
|
|
9
|
+
import { STATUS_CODES, createServer as createServer$1, get, request } from "node:http";
|
|
10
10
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
11
11
|
import { clientRequestSchema } from "@deepseek-ai/dsh-client-connection";
|
|
12
12
|
import { SessionId } from "@deepseek-ai/dsh-session/types";
|
|
@@ -16,6 +16,12 @@ import { parseFileAddress } from "@deepseek-ai/dsh-util-workspace-path";
|
|
|
16
16
|
const RPC_CHANNEL = "/api";
|
|
17
17
|
const RPC_PREFIX = "langify-code-server/";
|
|
18
18
|
/**
|
|
19
|
+
* Where the page loads the editor from. `dsh` serves it under DSH's own origin;
|
|
20
|
+
* `desktop` is the plugin's own address on 127.0.0.1, for DSH Desktop, whose
|
|
21
|
+
* page is not on http(s) (see host/ingress/).
|
|
22
|
+
*/
|
|
23
|
+
const INGRESSES = ["dsh", "desktop"];
|
|
24
|
+
/**
|
|
19
25
|
* Why the extension could not open a file. `outcome_unknown` means the editor
|
|
20
26
|
* was already asked and may still show the file later.
|
|
21
27
|
*/
|
|
@@ -37,6 +43,7 @@ const HOST_ERROR_CODES = [
|
|
|
37
43
|
"different_host",
|
|
38
44
|
"code_server_not_found",
|
|
39
45
|
"code_server_failed",
|
|
46
|
+
"desktop_port_in_use",
|
|
40
47
|
"unknown_binding",
|
|
41
48
|
"not_ready",
|
|
42
49
|
"binding_disconnected",
|
|
@@ -53,7 +60,8 @@ const binding = {
|
|
|
53
60
|
const requestSchemas = {
|
|
54
61
|
create: z.strictObject({
|
|
55
62
|
sessionId: id,
|
|
56
|
-
ownerId: id
|
|
63
|
+
ownerId: id,
|
|
64
|
+
ingress: z.enum(INGRESSES)
|
|
57
65
|
}),
|
|
58
66
|
watch: z.strictObject({
|
|
59
67
|
...binding,
|
|
@@ -584,102 +592,24 @@ function healthy(socketPath) {
|
|
|
584
592
|
}).on("error", () => resolve(false));
|
|
585
593
|
});
|
|
586
594
|
}
|
|
587
|
-
// src/host/relay.ts
|
|
588
|
-
/** Path prefix code-server is served under, relative to the DSH page. */
|
|
589
|
-
const RELAY_PREFIX = "/code-server";
|
|
590
|
-
/** DSH's credentials are not code-server's business. */
|
|
591
|
-
const PRIVATE_HEADERS = /* @__PURE__ */ new Set(["cookie", "authorization"]);
|
|
592
|
-
function upstreamHeaders(headers) {
|
|
593
|
-
return Object.fromEntries(Object.entries(headers).filter(([name]) => !PRIVATE_HEADERS.has(name)));
|
|
594
|
-
}
|
|
595
|
-
function upstreamPath(url = "/") {
|
|
596
|
-
const path = url.slice(12);
|
|
597
|
-
return path.startsWith("/") ? path : `/${path}`;
|
|
598
|
-
}
|
|
599
|
-
/** Handler for HTTP requests below {@link RELAY_PREFIX}. */
|
|
600
|
-
function relayRequests(admission, start) {
|
|
601
|
-
return async (req, res) => {
|
|
602
|
-
const rejection = admission({ headers: req.headers });
|
|
603
|
-
if (rejection !== void 0) {
|
|
604
|
-
res.writeHead(rejection).end();
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
let server;
|
|
608
|
-
try {
|
|
609
|
-
server = await start();
|
|
610
|
-
} catch (error) {
|
|
611
|
-
res.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
|
|
612
|
-
res.end(error instanceof Error ? error.message : "code-server is unavailable");
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
615
|
-
const upstream = request({
|
|
616
|
-
socketPath: server.socketPath,
|
|
617
|
-
method: req.method,
|
|
618
|
-
path: upstreamPath(req.url),
|
|
619
|
-
headers: upstreamHeaders(req.headers)
|
|
620
|
-
}, (response) => {
|
|
621
|
-
res.writeHead(response.statusCode ?? 502, response.statusMessage, response.headers);
|
|
622
|
-
response.pipe(res);
|
|
623
|
-
});
|
|
624
|
-
upstream.on("error", () => {
|
|
625
|
-
if (res.headersSent) res.destroy();
|
|
626
|
-
else res.writeHead(502).end();
|
|
627
|
-
});
|
|
628
|
-
res.on("close", () => upstream.destroy());
|
|
629
|
-
req.pipe(upstream);
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
/** Handler for code-server's WebSocket upgrades, which arrive on one exact path. */
|
|
633
|
-
function relayUpgrades(admission, socketPath) {
|
|
634
|
-
return (req, socket, head) => {
|
|
635
|
-
const rejection = admission({ headers: req.headers });
|
|
636
|
-
if (rejection !== void 0) {
|
|
637
|
-
socket.end(`HTTP/1.1 ${rejection} ${STATUS_CODES[rejection] ?? ""}\r\n\r\n`);
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
|
-
const upstream = request({
|
|
641
|
-
socketPath,
|
|
642
|
-
method: req.method,
|
|
643
|
-
path: upstreamPath(req.url),
|
|
644
|
-
headers: upstreamHeaders(req.headers)
|
|
645
|
-
});
|
|
646
|
-
upstream.on("upgrade", (response, upstreamSocket, upstreamHead) => {
|
|
647
|
-
const lines = [`HTTP/1.1 ${response.statusCode ?? 101} ${response.statusMessage ?? ""}`];
|
|
648
|
-
for (let index = 0; index < response.rawHeaders.length; index += 2) lines.push(`${response.rawHeaders[index] ?? ""}: ${response.rawHeaders[index + 1] ?? ""}`);
|
|
649
|
-
socket.write(`${lines.join("\r\n")}\r\n\r\n`);
|
|
650
|
-
if (upstreamHead.length > 0) socket.write(upstreamHead);
|
|
651
|
-
if (head.length > 0) upstreamSocket.write(head);
|
|
652
|
-
upstreamSocket.pipe(socket).pipe(upstreamSocket);
|
|
653
|
-
upstreamSocket.on("error", () => socket.destroy());
|
|
654
|
-
upstreamSocket.on("close", () => socket.destroy());
|
|
655
|
-
socket.on("close", () => upstreamSocket.destroy());
|
|
656
|
-
});
|
|
657
|
-
upstream.on("response", (response) => {
|
|
658
|
-
response.resume();
|
|
659
|
-
socket.end(`HTTP/1.1 ${response.statusCode ?? 502} ${response.statusMessage ?? ""}\r\n\r\n`);
|
|
660
|
-
});
|
|
661
|
-
upstream.on("error", () => socket.destroy());
|
|
662
|
-
socket.on("error", () => upstream.destroy());
|
|
663
|
-
upstream.end();
|
|
664
|
-
};
|
|
665
|
-
}
|
|
666
595
|
// src/host/handlers.ts
|
|
667
596
|
/**
|
|
668
597
|
* One exact route per method on DSH's shared RPC channel. DSH authenticates the
|
|
669
598
|
* browser before a request reaches these routes.
|
|
670
599
|
*/
|
|
671
|
-
function rpcRoutes(services, bridge, codeServer) {
|
|
600
|
+
function rpcRoutes(services, bridge, codeServer, editors) {
|
|
672
601
|
const handlers = {
|
|
673
|
-
async create({ sessionId, ownerId }, signal) {
|
|
602
|
+
async create({ sessionId, ownerId, ingress }, signal) {
|
|
674
603
|
const { cwd } = await sessionFolders(services, sessionId, signal);
|
|
675
604
|
const folder = await localPath(services, cwd, cwd, "directory", signal);
|
|
676
605
|
await codeServer.ensure();
|
|
606
|
+
const base = await editors[ingress]();
|
|
677
607
|
signal.throwIfAborted();
|
|
678
608
|
const bindingId = bridge.create(sessionId, ownerId, folder);
|
|
679
609
|
return {
|
|
680
610
|
bindingId,
|
|
681
611
|
cwd: folder,
|
|
682
|
-
url: editorUrl(bindingId, folder)
|
|
612
|
+
url: editorUrl(base, bindingId, folder)
|
|
683
613
|
};
|
|
684
614
|
},
|
|
685
615
|
async watch({ sessionId, bindingId, revision }, signal) {
|
|
@@ -746,10 +676,9 @@ function rpcRoutes(services, bridge, codeServer) {
|
|
|
746
676
|
}
|
|
747
677
|
}));
|
|
748
678
|
}
|
|
749
|
-
/** The iframe URL
|
|
750
|
-
function editorUrl(bindingId, cwd) {
|
|
751
|
-
|
|
752
|
-
return `${RELAY_PREFIX.slice(1)}/?${query.toString()}#${BINDING_FRAGMENT}=${bindingId}`;
|
|
679
|
+
/** The iframe URL; its fragment tells the window's extension its binding. */
|
|
680
|
+
function editorUrl(base, bindingId, cwd) {
|
|
681
|
+
return `${base}?${new URLSearchParams({ folder: cwd }).toString()}#${BINDING_FRAGMENT}=${bindingId}`;
|
|
753
682
|
}
|
|
754
683
|
function asCodeServerError(error) {
|
|
755
684
|
if (error instanceof CodeServerError) return error;
|
|
@@ -792,6 +721,165 @@ async function localPath(services, path, base, type, signal) {
|
|
|
792
721
|
if (services.fs.processPathFromHostPath(processPath) !== processPath) throw new CodeServerError("different_host", "The session runs on another file system");
|
|
793
722
|
return realpath(processPath);
|
|
794
723
|
}
|
|
724
|
+
// src/host/relay.ts
|
|
725
|
+
/** Credentials belong to the ingress, not to code-server. */
|
|
726
|
+
const PRIVATE_HEADERS = /* @__PURE__ */ new Set(["cookie", "authorization"]);
|
|
727
|
+
function forward(req, prefix, socketPath) {
|
|
728
|
+
const path = (req.url ?? "/").slice(prefix.length);
|
|
729
|
+
return request({
|
|
730
|
+
socketPath,
|
|
731
|
+
method: req.method,
|
|
732
|
+
path: path.startsWith("/") ? path : `/${path}`,
|
|
733
|
+
headers: Object.fromEntries(Object.entries(req.headers).filter(([name]) => !PRIVATE_HEADERS.has(name)))
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
/** Handler for HTTP requests below the prefix. */
|
|
737
|
+
function relayRequests({ prefix, admit, start }) {
|
|
738
|
+
return async (req, res) => {
|
|
739
|
+
const refusal = admit(req);
|
|
740
|
+
if (refusal !== void 0) {
|
|
741
|
+
res.writeHead(refusal).end();
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
let server;
|
|
745
|
+
try {
|
|
746
|
+
server = await start();
|
|
747
|
+
} catch (error) {
|
|
748
|
+
res.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
|
|
749
|
+
res.end(error instanceof Error ? error.message : "code-server is unavailable");
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
const upstream = forward(req, prefix, server.socketPath);
|
|
753
|
+
upstream.on("response", (response) => {
|
|
754
|
+
res.writeHead(response.statusCode ?? 502, response.statusMessage, response.headers);
|
|
755
|
+
response.pipe(res);
|
|
756
|
+
});
|
|
757
|
+
upstream.on("error", () => {
|
|
758
|
+
if (res.headersSent) res.destroy();
|
|
759
|
+
else res.writeHead(502).end();
|
|
760
|
+
});
|
|
761
|
+
res.on("close", () => upstream.destroy());
|
|
762
|
+
req.pipe(upstream);
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
function refuse(socket, status) {
|
|
766
|
+
socket.end(`HTTP/1.1 ${status} ${STATUS_CODES[status] ?? ""}\r\n\r\n`);
|
|
767
|
+
}
|
|
768
|
+
/** Handler for WebSocket upgrades below the prefix. */
|
|
769
|
+
function relayUpgrades({ prefix, admit, start }) {
|
|
770
|
+
return (req, socket, head) => {
|
|
771
|
+
const refusal = admit(req);
|
|
772
|
+
if (refusal !== void 0) return refuse(socket, refusal);
|
|
773
|
+
socket.on("error", () => socket.destroy());
|
|
774
|
+
start().then(({ socketPath }) => {
|
|
775
|
+
if (socket.destroyed) return;
|
|
776
|
+
const upstream = forward(req, prefix, socketPath);
|
|
777
|
+
upstream.on("upgrade", (response, upstreamSocket, upstreamHead) => {
|
|
778
|
+
const lines = [`HTTP/1.1 ${response.statusCode ?? 101} ${response.statusMessage ?? ""}`];
|
|
779
|
+
for (let index = 0; index < response.rawHeaders.length; index += 2) lines.push(`${response.rawHeaders[index] ?? ""}: ${response.rawHeaders[index + 1] ?? ""}`);
|
|
780
|
+
socket.write(`${lines.join("\r\n")}\r\n\r\n`);
|
|
781
|
+
if (upstreamHead.length > 0) socket.write(upstreamHead);
|
|
782
|
+
if (head.length > 0) upstreamSocket.write(head);
|
|
783
|
+
upstreamSocket.pipe(socket).pipe(upstreamSocket);
|
|
784
|
+
upstreamSocket.on("error", () => socket.destroy());
|
|
785
|
+
upstreamSocket.on("close", () => socket.destroy());
|
|
786
|
+
socket.on("close", () => upstreamSocket.destroy());
|
|
787
|
+
});
|
|
788
|
+
upstream.on("response", (response) => {
|
|
789
|
+
response.resume();
|
|
790
|
+
refuse(socket, response.statusCode ?? 502);
|
|
791
|
+
});
|
|
792
|
+
upstream.on("error", () => socket.destroy());
|
|
793
|
+
socket.on("close", () => upstream.destroy());
|
|
794
|
+
upstream.end();
|
|
795
|
+
}, () => refuse(socket, 502));
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
// src/host/ingress/desktop.ts
|
|
799
|
+
const HOST = "127.0.0.1";
|
|
800
|
+
var DesktopIngress = class {
|
|
801
|
+
#port;
|
|
802
|
+
#start;
|
|
803
|
+
#token = randomBytes(32).toString("base64url");
|
|
804
|
+
#listening;
|
|
805
|
+
/** @param port - 0 picks a free port, which changes the editor's origin on every run. */
|
|
806
|
+
constructor(port, start) {
|
|
807
|
+
this.#port = port;
|
|
808
|
+
this.#start = start;
|
|
809
|
+
}
|
|
810
|
+
/** The editor's address, listening from the first call on. */
|
|
811
|
+
async base() {
|
|
812
|
+
this.#listening ??= this.#listen().catch((error) => {
|
|
813
|
+
this.#listening = void 0;
|
|
814
|
+
throw error;
|
|
815
|
+
});
|
|
816
|
+
return `${(await this.#listening).origin}/${this.#token}/`;
|
|
817
|
+
}
|
|
818
|
+
async dispose() {
|
|
819
|
+
const listener = await this.#listening?.catch(() => void 0);
|
|
820
|
+
this.#listening = void 0;
|
|
821
|
+
if (listener === void 0) return;
|
|
822
|
+
const { server } = listener;
|
|
823
|
+
const closed = new Promise((resolve) => server.close(() => resolve()));
|
|
824
|
+
server.closeAllConnections();
|
|
825
|
+
await closed;
|
|
826
|
+
}
|
|
827
|
+
async #listen() {
|
|
828
|
+
let host = "";
|
|
829
|
+
const relay = {
|
|
830
|
+
prefix: `/${this.#token}`,
|
|
831
|
+
admit: (req) => this.#admit(req, host),
|
|
832
|
+
start: this.#start
|
|
833
|
+
};
|
|
834
|
+
const handle = relayRequests(relay);
|
|
835
|
+
const server = createServer$1((req, res) => void handle(req, res));
|
|
836
|
+
server.on("upgrade", relayUpgrades(relay));
|
|
837
|
+
await new Promise((resolve, reject) => {
|
|
838
|
+
server.once("error", reject);
|
|
839
|
+
server.listen(this.#port, HOST, () => {
|
|
840
|
+
server.off("error", reject);
|
|
841
|
+
resolve();
|
|
842
|
+
});
|
|
843
|
+
}).catch((error) => {
|
|
844
|
+
if ((error instanceof Error && "code" in error ? error.code : void 0) !== "EADDRINUSE") throw error;
|
|
845
|
+
throw new CodeServerError("desktop_port_in_use", `${HOST}:${this.#port}`, { cause: error });
|
|
846
|
+
});
|
|
847
|
+
const address = server.address();
|
|
848
|
+
host = `${HOST}:${typeof address === "object" && address !== null ? address.port : this.#port}`;
|
|
849
|
+
return {
|
|
850
|
+
server,
|
|
851
|
+
origin: `http://${host}`
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
/** @param host - the listener's own host and port. */
|
|
855
|
+
#admit(req, host) {
|
|
856
|
+
if (req.headers.host !== host) return 403;
|
|
857
|
+
const token = Buffer.from((req.url ?? "").split("/", 2)[1] ?? "");
|
|
858
|
+
const expected = Buffer.from(this.#token);
|
|
859
|
+
return token.length === expected.length && timingSafeEqual(token, expected) ? void 0 : 404;
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
// src/host/ingress/dsh.ts
|
|
863
|
+
const PREFIX = "/code-server";
|
|
864
|
+
function dshIngress(ctx, start) {
|
|
865
|
+
const relay = {
|
|
866
|
+
prefix: PREFIX,
|
|
867
|
+
admit: (req) => ctx.connection.requestRejection({ headers: req.headers }),
|
|
868
|
+
start
|
|
869
|
+
};
|
|
870
|
+
ctx.effect(() => ctx.webServer.register({
|
|
871
|
+
kind: "prefix",
|
|
872
|
+
path: PREFIX,
|
|
873
|
+
handler: relayRequests(relay)
|
|
874
|
+
}), "code-server: DSH ingress");
|
|
875
|
+
return {
|
|
876
|
+
base: `${PREFIX.slice(1)}/`,
|
|
877
|
+
serveWebSocket: ({ commit }) => ctx.effect(() => ctx.webServer.registerUpgrade({
|
|
878
|
+
path: `${PREFIX}/stable-${commit}`,
|
|
879
|
+
handler: relayUpgrades(relay)
|
|
880
|
+
}), "code-server: DSH ingress WebSocket")
|
|
881
|
+
};
|
|
882
|
+
}
|
|
795
883
|
// src/host/services.ts
|
|
796
884
|
const inject = [
|
|
797
885
|
"connection",
|
|
@@ -808,7 +896,8 @@ const name = "langify-code-server";
|
|
|
808
896
|
const Config = Schema.object({
|
|
809
897
|
command: Schema.string().default("code-server").description("code-server executable name or path."),
|
|
810
898
|
userDataDir: Schema.string().default("").description("code-server's --user-data-dir. Empty uses its default."),
|
|
811
|
-
extensionsDir: Schema.string().default("").description("code-server's --extensions-dir. Empty uses its default.")
|
|
899
|
+
extensionsDir: Schema.string().default("").description("code-server's --extensions-dir. Empty uses its default."),
|
|
900
|
+
desktopPort: Schema.natural().max(65535).default(41820).description("Port on 127.0.0.1 where DSH Desktop loads the editor.")
|
|
812
901
|
});
|
|
813
902
|
async function apply(ctx, config) {
|
|
814
903
|
const runtimeDir = await mkdtemp(join(process.env.XDG_RUNTIME_DIR ?? tmpdir(), "dsh-code-server-"));
|
|
@@ -818,22 +907,21 @@ async function apply(ctx, config) {
|
|
|
818
907
|
}), "code-server: runtime directory");
|
|
819
908
|
const bridge = await Bridge.listen(join(runtimeDir, "bridge.sock"));
|
|
820
909
|
ctx.effect(() => () => bridge.dispose(), "code-server: bridge");
|
|
821
|
-
const
|
|
910
|
+
const start = () => codeServer.ensure();
|
|
911
|
+
const dsh = dshIngress(ctx, start);
|
|
912
|
+
const desktop = new DesktopIngress(config.desktopPort, start);
|
|
913
|
+
ctx.effect(() => () => desktop.dispose(), "code-server: Desktop ingress");
|
|
822
914
|
const codeServer = new CodeServer(ctx.subprocess, {
|
|
823
915
|
command: config.command,
|
|
824
916
|
userDataDir: config.userDataDir || void 0,
|
|
825
917
|
extensionsDir: config.extensionsDir || void 0,
|
|
826
918
|
env: { [SOCKET_ENV]: bridge.socketPath }
|
|
827
|
-
}, join(runtimeDir, "code-server.sock"), fileURLToPath(new URL("../extension/dsh-code-bridge.vsix", import.meta.url)),
|
|
828
|
-
path: `${RELAY_PREFIX}/stable-${commit}`,
|
|
829
|
-
handler: relayUpgrades(admission, socketPath)
|
|
830
|
-
}), "code-server: WebSocket relay"));
|
|
919
|
+
}, join(runtimeDir, "code-server.sock"), fileURLToPath(new URL("../extension/dsh-code-bridge.vsix", import.meta.url)), dsh.serveWebSocket);
|
|
831
920
|
ctx.effect(() => () => codeServer.dispose(), "code-server: process");
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
for (const route of rpcRoutes(ctx, bridge, codeServer)) ctx.effect(() => ctx.connection.fetch.register(route), `code-server: ${route.path}`);
|
|
921
|
+
const editors = {
|
|
922
|
+
dsh: async () => dsh.base,
|
|
923
|
+
desktop: () => desktop.base()
|
|
924
|
+
};
|
|
925
|
+
for (const route of rpcRoutes(ctx, bridge, codeServer, editors)) ctx.effect(() => ctx.connection.fetch.register(route), `code-server: ${route.path}`);
|
|
838
926
|
}
|
|
839
927
|
export { Config, apply, inject, name };
|
|
Binary file
|
package/package.json
CHANGED