coding-tool-x 3.5.6 → 3.5.7
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.md +17 -0
- package/bin/ctx.js +6 -1
- package/dist/web/assets/{Analytics-CRNCHeui.js → Analytics-C6DEmD3D.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-C0erJdo2.js → ConfigTemplates-Cf_iTpC4.js} +1 -1
- package/dist/web/assets/{Home-CL5z6Q4d.js → Home-BtBmYLJ1.js} +1 -1
- package/dist/web/assets/{PluginManager-hDx0XMO_.js → PluginManager-DEk8vSw5.js} +1 -1
- package/dist/web/assets/{ProjectList-BNsz96av.js → ProjectList-BMVhA_Kh.js} +1 -1
- package/dist/web/assets/{SessionList-CG1UhFo3.js → SessionList-B5ioAXxg.js} +1 -1
- package/dist/web/assets/{SkillManager-D6Vwpajh.js → SkillManager-DcZOiiSf.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-C3TjeOPy.js → WorkspaceManager-BHqI8aGV.js} +1 -1
- package/dist/web/assets/{index-GuER-BmS.js → index-CtByKdkA.js} +2 -2
- package/dist/web/index.html +1 -1
- package/docs/Caddyfile.example +19 -0
- package/docs/reverse-proxy-https.md +57 -0
- package/package.json +2 -1
- package/src/commands/daemon.js +33 -5
- package/src/commands/ui.js +12 -3
- package/src/config/paths.js +6 -0
- package/src/index.js +124 -34
- package/src/server/index.js +25 -5
- package/src/server/services/https-cert.js +171 -0
- package/src/server/services/network-access.js +47 -2
- package/src/server/services/notification-hooks.js +8 -2
- package/src/server/services/web-ui-runtime.js +54 -0
- package/src/server/websocket-server.js +11 -4
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const net = require('net');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const selfsigned = require('selfsigned');
|
|
6
|
+
const { PATHS } = require('../../config/paths');
|
|
7
|
+
|
|
8
|
+
function dedupeSorted(values = []) {
|
|
9
|
+
return [...new Set(values.filter(Boolean).map((value) => String(value).trim()).filter(Boolean))].sort();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isIpAddress(value) {
|
|
13
|
+
return net.isIP(String(value || '').trim()) !== 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isValidDnsLabel(value) {
|
|
17
|
+
return /^[A-Za-z0-9.-]+$/.test(String(value || '').trim());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function collectLocalCertificateHosts() {
|
|
21
|
+
const hosts = ['localhost', '127.0.0.1', '::1'];
|
|
22
|
+
const hostname = os.hostname();
|
|
23
|
+
if (isValidDnsLabel(hostname)) {
|
|
24
|
+
hosts.push(hostname);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const interfaces = os.networkInterfaces();
|
|
28
|
+
Object.values(interfaces).forEach((entries) => {
|
|
29
|
+
(entries || []).forEach((entry) => {
|
|
30
|
+
const address = String(entry?.address || '').trim();
|
|
31
|
+
if (!address) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (entry?.family === 'IPv4' || entry?.family === 4) {
|
|
36
|
+
hosts.push(address);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if ((entry?.family === 'IPv6' || entry?.family === 6) && address !== '::1' && !address.startsWith('fe80::')) {
|
|
41
|
+
hosts.push(address);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return dedupeSorted(hosts);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildSubjectAltNames(hosts = collectLocalCertificateHosts()) {
|
|
50
|
+
return dedupeSorted(hosts).map((value) => (
|
|
51
|
+
isIpAddress(value)
|
|
52
|
+
? { type: 7, ip: value }
|
|
53
|
+
: { type: 2, value }
|
|
54
|
+
));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function ensureParentDir(filePath) {
|
|
58
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readMeta(metaPath) {
|
|
62
|
+
if (!fs.existsSync(metaPath)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function needsRegeneration(hosts, meta) {
|
|
74
|
+
if (!meta || !Array.isArray(meta.hosts)) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
const currentHosts = dedupeSorted(hosts);
|
|
78
|
+
const storedHosts = dedupeSorted(meta.hosts);
|
|
79
|
+
return currentHosts.length !== storedHosts.length || currentHosts.some((value, index) => value !== storedHosts[index]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function generateSelfSignedCertificate(hosts) {
|
|
83
|
+
const primaryHost = hosts.find((value) => value === '127.0.0.1')
|
|
84
|
+
|| hosts.find((value) => !isIpAddress(value))
|
|
85
|
+
|| hosts[0]
|
|
86
|
+
|| '127.0.0.1';
|
|
87
|
+
|
|
88
|
+
const attrs = [{ name: 'commonName', value: primaryHost }];
|
|
89
|
+
return selfsigned.generate(attrs, {
|
|
90
|
+
algorithm: 'sha256',
|
|
91
|
+
days: 3650,
|
|
92
|
+
keySize: 2048,
|
|
93
|
+
extensions: [
|
|
94
|
+
{ name: 'basicConstraints', cA: false },
|
|
95
|
+
{
|
|
96
|
+
name: 'keyUsage',
|
|
97
|
+
digitalSignature: true,
|
|
98
|
+
keyEncipherment: true
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'extKeyUsage',
|
|
102
|
+
serverAuth: true
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'subjectAltName',
|
|
106
|
+
altNames: buildSubjectAltNames(hosts)
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function ensureLocalHttpsCertificate(options = {}) {
|
|
113
|
+
const keyPath = options.keyPath || PATHS.https.key;
|
|
114
|
+
const certPath = options.certPath || PATHS.https.cert;
|
|
115
|
+
const metaPath = options.metaPath || PATHS.https.meta;
|
|
116
|
+
const hosts = dedupeSorted(options.hosts || collectLocalCertificateHosts());
|
|
117
|
+
const existingMeta = readMeta(metaPath);
|
|
118
|
+
const hasFiles = fs.existsSync(keyPath) && fs.existsSync(certPath);
|
|
119
|
+
|
|
120
|
+
if (hasFiles && !needsRegeneration(hosts, existingMeta)) {
|
|
121
|
+
return {
|
|
122
|
+
keyPath,
|
|
123
|
+
certPath,
|
|
124
|
+
metaPath,
|
|
125
|
+
hosts,
|
|
126
|
+
generated: false
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
ensureParentDir(keyPath);
|
|
131
|
+
ensureParentDir(certPath);
|
|
132
|
+
ensureParentDir(metaPath);
|
|
133
|
+
|
|
134
|
+
const certificate = generateSelfSignedCertificate(hosts);
|
|
135
|
+
fs.writeFileSync(keyPath, certificate.private, 'utf8');
|
|
136
|
+
fs.writeFileSync(certPath, certificate.cert, 'utf8');
|
|
137
|
+
fs.writeFileSync(metaPath, JSON.stringify({
|
|
138
|
+
hosts,
|
|
139
|
+
generatedAt: new Date().toISOString()
|
|
140
|
+
}, null, 2), 'utf8');
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
keyPath,
|
|
144
|
+
certPath,
|
|
145
|
+
metaPath,
|
|
146
|
+
hosts,
|
|
147
|
+
generated: true
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getLocalHttpsCredentials(options = {}) {
|
|
152
|
+
const certificate = ensureLocalHttpsCertificate(options);
|
|
153
|
+
return {
|
|
154
|
+
...certificate,
|
|
155
|
+
key: fs.readFileSync(certificate.keyPath),
|
|
156
|
+
cert: fs.readFileSync(certificate.certPath)
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
ensureLocalHttpsCertificate,
|
|
162
|
+
getLocalHttpsCredentials,
|
|
163
|
+
_test: {
|
|
164
|
+
dedupeSorted,
|
|
165
|
+
isIpAddress,
|
|
166
|
+
isValidDnsLabel,
|
|
167
|
+
collectLocalCertificateHosts,
|
|
168
|
+
buildSubjectAltNames,
|
|
169
|
+
needsRegeneration
|
|
170
|
+
}
|
|
171
|
+
};
|
|
@@ -16,6 +16,21 @@ function isLoopbackAddress(address) {
|
|
|
16
16
|
return false;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function hasTrustedProxySocket(req) {
|
|
20
|
+
if (!req) return false;
|
|
21
|
+
return isLoopbackAddress(req.socket && req.socket.remoteAddress);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getForwardedHeaderValue(req, headerName) {
|
|
25
|
+
if (!hasTrustedProxySocket(req) || !req.headers) {
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const rawValue = req.headers[headerName];
|
|
30
|
+
const value = Array.isArray(rawValue) ? rawValue[0] : rawValue;
|
|
31
|
+
return String(value || '').split(',')[0].trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
19
34
|
function isLoopbackRequest(req) {
|
|
20
35
|
if (!req) return false;
|
|
21
36
|
const socketAddress = req.socket && req.socket.remoteAddress;
|
|
@@ -30,6 +45,34 @@ function isLoopbackRequest(req) {
|
|
|
30
45
|
return true;
|
|
31
46
|
}
|
|
32
47
|
|
|
48
|
+
function getRequestProtocol(req) {
|
|
49
|
+
const forwardedProto = getForwardedHeaderValue(req, 'x-forwarded-proto').toLowerCase();
|
|
50
|
+
if (forwardedProto === 'https' || forwardedProto === 'https:') {
|
|
51
|
+
return 'https:';
|
|
52
|
+
}
|
|
53
|
+
if (forwardedProto === 'http' || forwardedProto === 'http:') {
|
|
54
|
+
return 'http:';
|
|
55
|
+
}
|
|
56
|
+
return req && req.socket && req.socket.encrypted ? 'https:' : 'http:';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getRequestHost(req) {
|
|
60
|
+
const forwardedHost = getForwardedHeaderValue(req, 'x-forwarded-host');
|
|
61
|
+
if (forwardedHost) {
|
|
62
|
+
return forwardedHost;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!req || !req.headers) {
|
|
66
|
+
return '';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const host = req.headers.host;
|
|
70
|
+
if (Array.isArray(host)) {
|
|
71
|
+
return String(host[0] || '').trim();
|
|
72
|
+
}
|
|
73
|
+
return String(host || '').trim();
|
|
74
|
+
}
|
|
75
|
+
|
|
33
76
|
function isSameOriginRequest(req) {
|
|
34
77
|
if (!req) return false;
|
|
35
78
|
const origin = req.headers && req.headers.origin;
|
|
@@ -37,14 +80,14 @@ function isSameOriginRequest(req) {
|
|
|
37
80
|
return true;
|
|
38
81
|
}
|
|
39
82
|
|
|
40
|
-
const host = req
|
|
83
|
+
const host = getRequestHost(req);
|
|
41
84
|
if (!host) {
|
|
42
85
|
return false;
|
|
43
86
|
}
|
|
44
87
|
|
|
45
88
|
try {
|
|
46
89
|
const originUrl = new URL(origin);
|
|
47
|
-
return originUrl.host === host;
|
|
90
|
+
return originUrl.host.toLowerCase() === host.toLowerCase();
|
|
48
91
|
} catch (error) {
|
|
49
92
|
return false;
|
|
50
93
|
}
|
|
@@ -124,6 +167,8 @@ module.exports = {
|
|
|
124
167
|
normalizeAddress,
|
|
125
168
|
isLoopbackAddress,
|
|
126
169
|
isLoopbackRequest,
|
|
170
|
+
getRequestProtocol,
|
|
171
|
+
getRequestHost,
|
|
127
172
|
isSameOriginRequest,
|
|
128
173
|
isRemoteMutationAllowed,
|
|
129
174
|
createRemoteMutationGuard,
|
|
@@ -9,6 +9,7 @@ const { loadConfig } = require('../../config/loader');
|
|
|
9
9
|
const { loadUIConfig, saveUIConfig } = require('./ui-config');
|
|
10
10
|
const codexSettingsManager = require('./codex-settings-manager');
|
|
11
11
|
const geminiSettingsManager = require('./gemini-settings-manager');
|
|
12
|
+
const { getWebUiProtocol } = require('./web-ui-runtime');
|
|
12
13
|
|
|
13
14
|
const MANAGED_HOOK_NAME = 'coding-tool-notify';
|
|
14
15
|
const MANAGED_OPENCODE_PLUGIN_FILE = 'coding-tool-notify.js';
|
|
@@ -169,9 +170,10 @@ function buildBrowserNotificationEndpoint() {
|
|
|
169
170
|
const config = loadConfig();
|
|
170
171
|
const port = Number(config?.ports?.webUI);
|
|
171
172
|
const resolvedPort = Number.isFinite(port) && port > 0 ? port : 19999;
|
|
172
|
-
|
|
173
|
+
const protocol = getWebUiProtocol();
|
|
174
|
+
return `${protocol}://127.0.0.1:${resolvedPort}/api/hooks/browser-event`;
|
|
173
175
|
} catch (error) {
|
|
174
|
-
return
|
|
176
|
+
return `${getWebUiProtocol()}://127.0.0.1:19999/api/hooks/browser-event`;
|
|
175
177
|
}
|
|
176
178
|
}
|
|
177
179
|
|
|
@@ -746,6 +748,10 @@ function sendBrowserNotification(payload) {
|
|
|
746
748
|
timeout: 5000
|
|
747
749
|
}
|
|
748
750
|
|
|
751
|
+
if (urlObj.protocol === 'https:' && ['127.0.0.1', 'localhost', '::1'].includes(urlObj.hostname)) {
|
|
752
|
+
options.rejectUnauthorized = false
|
|
753
|
+
}
|
|
754
|
+
|
|
749
755
|
const requestModule = urlObj.protocol === 'https:' ? https : http
|
|
750
756
|
const request = requestModule.request(options, () => resolve())
|
|
751
757
|
request.on('error', () => resolve())
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
function normalizeWebUiProtocol(value) {
|
|
2
|
+
return String(value || '').trim().toLowerCase() === 'https' ? 'https' : 'http';
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function hasHttpsFlag(argv = process.argv) {
|
|
6
|
+
return Array.isArray(argv) && argv.includes('--https');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function getWebUiProtocol(options = {}) {
|
|
10
|
+
if (typeof options.https === 'boolean') {
|
|
11
|
+
return options.https ? 'https' : 'http';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (typeof options.protocol === 'string' && options.protocol.trim()) {
|
|
15
|
+
return normalizeWebUiProtocol(options.protocol);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const env = options.env || process.env;
|
|
19
|
+
if (env && typeof env.CC_TOOL_WEB_UI_PROTOCOL === 'string' && env.CC_TOOL_WEB_UI_PROTOCOL.trim()) {
|
|
20
|
+
return normalizeWebUiProtocol(env.CC_TOOL_WEB_UI_PROTOCOL);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return hasHttpsFlag(options.argv || process.argv) ? 'https' : 'http';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isHttpsEnabled(options = {}) {
|
|
27
|
+
return getWebUiProtocol(options) === 'https';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getDefaultLoopbackHost(protocol = 'http') {
|
|
31
|
+
return normalizeWebUiProtocol(protocol) === 'https' ? '127.0.0.1' : 'localhost';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getWebUiBaseUrl(port, options = {}) {
|
|
35
|
+
const protocol = getWebUiProtocol(options);
|
|
36
|
+
const hostname = String(options.hostname || '').trim() || getDefaultLoopbackHost(protocol);
|
|
37
|
+
return `${protocol}://${hostname}:${port}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getWebSocketBaseUrl(port, options = {}) {
|
|
41
|
+
const protocol = getWebUiProtocol(options) === 'https' ? 'wss' : 'ws';
|
|
42
|
+
const hostname = String(options.hostname || '').trim() || getDefaultLoopbackHost(protocol === 'wss' ? 'https' : 'http');
|
|
43
|
+
return `${protocol}://${hostname}:${port}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
normalizeWebUiProtocol,
|
|
48
|
+
hasHttpsFlag,
|
|
49
|
+
getWebUiProtocol,
|
|
50
|
+
isHttpsEnabled,
|
|
51
|
+
getDefaultLoopbackHost,
|
|
52
|
+
getWebUiBaseUrl,
|
|
53
|
+
getWebSocketBaseUrl
|
|
54
|
+
};
|
|
@@ -7,7 +7,9 @@ const { PATHS } = require('../config/paths');
|
|
|
7
7
|
const {
|
|
8
8
|
normalizeAddress,
|
|
9
9
|
isLoopbackAddress,
|
|
10
|
-
isLoopbackRequest
|
|
10
|
+
isLoopbackRequest,
|
|
11
|
+
getRequestProtocol,
|
|
12
|
+
getRequestHost
|
|
11
13
|
} = require('./services/network-access');
|
|
12
14
|
|
|
13
15
|
const MAX_PERSISTED_LOGS = 500;
|
|
@@ -111,8 +113,8 @@ function isAllowedWebSocketOrigin(req) {
|
|
|
111
113
|
return false;
|
|
112
114
|
}
|
|
113
115
|
|
|
114
|
-
const requestHost = parseHostHeader(req
|
|
115
|
-
const requestProtocol = req
|
|
116
|
+
const requestHost = parseHostHeader(getRequestHost(req));
|
|
117
|
+
const requestProtocol = getRequestProtocol(req);
|
|
116
118
|
const requestHostname = normalizeAddress(requestHost.hostname).toLowerCase();
|
|
117
119
|
const requestPort = requestHost.port || defaultPortForProtocol(requestProtocol);
|
|
118
120
|
|
|
@@ -534,5 +536,10 @@ module.exports = {
|
|
|
534
536
|
clearAllLogs,
|
|
535
537
|
broadcastProxyState,
|
|
536
538
|
broadcastSchedulerState,
|
|
537
|
-
broadcastBrowserNotification
|
|
539
|
+
broadcastBrowserNotification,
|
|
540
|
+
_test: {
|
|
541
|
+
parseHostHeader,
|
|
542
|
+
defaultPortForProtocol,
|
|
543
|
+
isAllowedWebSocketOrigin
|
|
544
|
+
}
|
|
538
545
|
};
|