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.
@@ -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.headers && req.headers.host;
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
- return `http://127.0.0.1:${resolvedPort}/api/hooks/browser-event`;
173
+ const protocol = getWebUiProtocol();
174
+ return `${protocol}://127.0.0.1:${resolvedPort}/api/hooks/browser-event`;
173
175
  } catch (error) {
174
- return 'http://127.0.0.1:19999/api/hooks/browser-event';
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.headers.host);
115
- const requestProtocol = req.socket && req.socket.encrypted ? 'https:' : 'http:';
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
  };