Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/API-Reference/editor/CodeHintManager.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ Test if a hint popup is open.
Register a handler to show hints at the top of the hint list.
This API allows extensions to add their own hints at the top of the standard hint list.

**Kind**: inner method of [<code>CodeHintManager</code>](#module_CodeHintManager)
**Kind**: inner method of [<code>CodeHintManager</code>](#module_CodeHintManager)

| Param | Type | Description |
| --- | --- | --- |
Expand All @@ -282,4 +282,4 @@ This API allows extensions to add their own hints at the top of the standard hin
### CodeHintManager.clearHintsAtTop()
Unregister the hints at top handler.

**Kind**: inner method of [<code>CodeHintManager</code>](#module_CodeHintManager)
**Kind**: inner method of [<code>CodeHintManager</code>](#module_CodeHintManager)
92 changes: 54 additions & 38 deletions serve-proxy.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env node
/* eslint-env node */

const http = require('http');
const https = require('https');
Expand All @@ -7,6 +8,10 @@
const fs = require('fs');
const httpProxy = require('http-proxy');

// Account server configuration - switch between local and production
const ACCOUNT_SERVER = 'https://account.phcode.dev'; // Production
// const ACCOUNT_SERVER = 'http://localhost:5000'; // Local development

// Default configuration
let config = {
port: 8000,
Expand All @@ -20,10 +25,10 @@
// Parse command line arguments
function parseArgs() {
const args = process.argv.slice(2);

for (let i = 0; i < args.length; i++) {
const arg = args[i];

if (arg === '-p' && args[i + 1]) {
config.port = parseInt(args[i + 1]);
i++;
Expand Down Expand Up @@ -64,42 +69,42 @@
});

// Modify proxy request headers
proxy.on('proxyReq', (proxyReq, req, res) => {
proxy.on('proxyReq', (proxyReq, req) => {
// Transform localhost:8000 to appear as phcode.dev domain
const originalHost = req.headers.host;
const originalReferer = req.headers.referer;
const originalOrigin = req.headers.origin;

// Set target host
proxyReq.setHeader('Host', 'account.phcode.dev');

const accountHost = new URL(ACCOUNT_SERVER).hostname;
proxyReq.setHeader('Host', accountHost);

// Transform referer from localhost:8000 to phcode.dev
if (originalReferer && originalReferer.includes('localhost:8000')) {
const newReferer = originalReferer.replace(/localhost:8000/g, 'phcode.dev');
proxyReq.setHeader('Referer', newReferer);
} else if (!originalReferer) {
proxyReq.setHeader('Referer', 'https://phcode.dev/');
}

// Transform origin from localhost:8000 to phcode.dev
if (originalOrigin && originalOrigin.includes('localhost:8000')) {
const newOrigin = originalOrigin.replace(/localhost:8000/g, 'phcode.dev');
proxyReq.setHeader('Origin', newOrigin);
} else if (!originalOrigin) {
proxyReq.setHeader('Origin', 'https://phcode.dev');
}

// Ensure HTTPS scheme
proxyReq.setHeader('X-Forwarded-Proto', 'https');
proxyReq.setHeader('X-Forwarded-For', req.connection.remoteAddress);

});

// Modify proxy response headers
proxy.on('proxyRes', (proxyRes, req, res) => {
// Pass through cache control and other security headers
// But translate any domain references back to localhost for the browser

const setCookieHeader = proxyRes.headers['set-cookie'];
if (setCookieHeader) {
// Transform any phcode.dev domain cookies back to localhost
Expand All @@ -108,7 +113,7 @@
});
proxyRes.headers['set-cookie'] = modifiedCookies;
}

// Ensure CORS headers if needed
if (config.cors) {
proxyRes.headers['Access-Control-Allow-Origin'] = '*';
Expand Down Expand Up @@ -148,7 +153,7 @@
res.end('File not found');
return;
}

if (stats.isDirectory()) {
// Try to serve index.html from directory
const indexPath = path.join(filePath, 'index.html');
Expand All @@ -163,7 +168,7 @@
res.end('Error reading directory');
return;
}

const html = `
<!DOCTYPE html>
<html>
Expand All @@ -178,56 +183,56 @@
</body>
</html>
`;

const headers = {
'Content-Type': 'text/html',
'Content-Length': Buffer.byteLength(html)
};

if (!config.cache) {
headers['Cache-Control'] = 'no-cache, no-store, must-revalidate';
headers['Pragma'] = 'no-cache';
headers['Expires'] = '0';
}

if (config.cors) {
headers['Access-Control-Allow-Origin'] = '*';
headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS';
headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control';
}

res.writeHead(200, headers);
res.end(html);
});
}
});
return;
}

// Serve file
const mimeType = getMimeType(filePath);
const headers = {
'Content-Type': mimeType,
'Content-Length': stats.size
};

if (!config.cache) {
headers['Cache-Control'] = 'no-cache, no-store, must-revalidate';
headers['Pragma'] = 'no-cache';
headers['Expires'] = '0';
}

if (config.cors) {
headers['Access-Control-Allow-Origin'] = '*';
headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS';
headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control';
}

res.writeHead(200, headers);

const stream = fs.createReadStream(filePath);
stream.pipe(res);

stream.on('error', (err) => {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Error reading file');
Expand All @@ -238,7 +243,7 @@
// Create HTTP server
const server = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url, true);

// Handle CORS preflight
if (req.method === 'OPTIONS' && config.cors) {
res.writeHead(200, {
Expand All @@ -249,46 +254,57 @@
res.end();
return;
}

// Check if this is a proxy request
if (parsedUrl.pathname.startsWith('/proxy/accounts')) {
// Extract the path after /proxy/accounts
const targetPath = parsedUrl.pathname.replace('/proxy/accounts', '');
const originalUrl = req.url;

// Modify the request URL for the proxy
req.url = targetPath + (parsedUrl.search || '');

if (!config.silent) {
console.log(`[PROXY] ${req.method} ${originalUrl} -> https://account.phcode.dev${req.url}`);
console.log(`[PROXY] ${req.method} ${originalUrl} -> ${ACCOUNT_SERVER}${req.url}`);
}

// Proxy the request
proxy.web(req, res, {
target: 'https://account.phcode.dev',
target: ACCOUNT_SERVER,
changeOrigin: true,
secure: true
});
return;
}

// Serve static files
let filePath = path.join(config.root, parsedUrl.pathname);

// Security: prevent directory traversal
const normalizedPath = path.normalize(filePath);
if (!normalizedPath.startsWith(config.root)) {
res.writeHead(403, { 'Content-Type': 'text/plain' });
res.end('Forbidden');
return;
}

if (!config.silent) {
const clientIp = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}${config.logIp ? ` (${clientIp})` : ''}`);
}

serveStaticFile(req, res, filePath);

// Handle directory requests without trailing slash
fs.stat(filePath, (err, stats) => {
if (err) {
serveStaticFile(req, res, filePath);
} else if (stats.isDirectory() && !parsedUrl.pathname.endsWith('/')) {
// Redirect to URL with trailing slash for directories
res.writeHead(301, { 'Location': req.url + '/' });

Check warning

Code scanning / CodeQL

Server-side URL redirect Medium

Untrusted URL redirection depends on a
user-provided value
.

Copilot Autofix

AI 6 months ago

The best way to fix this problem is to avoid using the raw req.url in the redirect and instead construct a canonical, relative URL path based solely on validated input. Specifically, when redirecting a user to the trailing slash version of a directory, parse and reconstruct the pathname—without including any potentially harmful user-supplied query strings or full URLs. The correct approach is to use the parsed pathname (from url.parse(req.url, true)) and ensure the result is a relative path (not an absolute URL). Additionally, you may want to re-attach the original query parameters (if any), to preserve search parameters when redirecting, but do so via encoding.

In practical terms, replace res.writeHead(301, { 'Location': req.url + '/' }); with a construct that starts with the URL-parsed pathname, appends the trailing slash, and (optionally) preserves the query string, ensuring the entire result is a relative path. All code changes can be made directly in serve-proxy.js at line 302, using standard Node.js modules you are already importing.

Suggested changeset 1
serve-proxy.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/serve-proxy.js b/serve-proxy.js
--- a/serve-proxy.js
+++ b/serve-proxy.js
@@ -299,7 +299,11 @@
             serveStaticFile(req, res, filePath);
         } else if (stats.isDirectory() && !parsedUrl.pathname.endsWith('/')) {
             // Redirect to URL with trailing slash for directories
-            res.writeHead(301, { 'Location': req.url + '/' });
+            let redirectPath = parsedUrl.pathname + '/';
+            if (parsedUrl.search) {
+                redirectPath += parsedUrl.search;
+            }
+            res.writeHead(301, { 'Location': redirectPath });
             res.end();
         } else {
             serveStaticFile(req, res, filePath);
EOF
@@ -299,7 +299,11 @@
serveStaticFile(req, res, filePath);
} else if (stats.isDirectory() && !parsedUrl.pathname.endsWith('/')) {
// Redirect to URL with trailing slash for directories
res.writeHead(301, { 'Location': req.url + '/' });
let redirectPath = parsedUrl.pathname + '/';
if (parsedUrl.search) {
redirectPath += parsedUrl.search;
}
res.writeHead(301, { 'Location': redirectPath });
res.end();
} else {
serveStaticFile(req, res, filePath);
Copilot is powered by AI and may make mistakes. Always verify output.
res.end();
} else {
serveStaticFile(req, res, filePath);
}
});
});

// Parse arguments and start server
Expand All @@ -300,7 +316,7 @@
console.log(`Available on:`);
console.log(` http://${config.host === '0.0.0.0' ? 'localhost' : config.host}:${config.port}`);
console.log(`Proxy routes:`);
console.log(` /proxy/accounts/* -> https://account.phcode.dev/*`);
console.log(` /proxy/accounts/* -> ${ACCOUNT_SERVER}/*`);
console.log('Hit CTRL-C to stop the server');
}
});
Expand All @@ -318,4 +334,4 @@
server.close(() => {
process.exit(0);
});
});
});
2 changes: 1 addition & 1 deletion src-node/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ const nodeConnector = NodeConnector.createNodeConnector(AUTH_CONNECTOR_ID, {
setVerificationCode
});

const ALLOWED_ORIGIN = 'https://account.phcode.io';
const ALLOWED_ORIGIN = 'https://account.phcode.dev';
function autoAuth(req, res) {
const origin = req.headers.origin;
// localhost dev of loginService is not allowed here to not leak to production. So autoAuth is not available in
Expand Down
3 changes: 2 additions & 1 deletion src/brackets.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ define(function (require, exports, module) {
require("widgets/InlineMenu");
require("thirdparty/tinycolor");
require("utils/LocalizationUtils");
require("services/login");
require("services/login-desktop");
require("services/login-browser");

// DEPRECATED: In future we want to remove the global CodeMirror, but for now we
// expose our required CodeMirror globally so as to avoid breaking extensions in the
Expand Down
6 changes: 3 additions & 3 deletions src/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
"app_title": "Phoenix Code",
"app_name_about": "Phoenix Code",
"about_icon": "styles/images/phoenix-icon.svg",
"account_url": "https://account.phcode.io/",
"account_url": "https://account.phcode.dev/",
"how_to_use_url": "https://github.com/adobe/brackets/wiki/How-to-Use-Brackets",
"docs_url": "https://docs.phcode.dev/",
"support_url": "https://account.phcode.io/?returnUrl=https%3A%2F%2Faccount.phcode.io%2F%23support",
"support_url": "https://account.phcode.dev/?returnUrl=https%3A%2F%2Faccount.phcode.dev%2F%23support",
"suggest_feature_url": "https://github.com/phcode-dev/phoenix/discussions/categories/ideas",
"report_issue_url": "https://github.com/phcode-dev/phoenix/issues/new/choose",
"get_involved_url": "https://github.com/phcode-dev/phoenix/discussions/77",
Expand Down Expand Up @@ -60,4 +60,4 @@
"url": "https://github.com/phcode-dev/phoenix/blob/master/LICENSE"
}
]
}
}
11 changes: 10 additions & 1 deletion src/nls/root/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -1594,7 +1594,7 @@ define({
"SIGNED_IN_FAILED_TITLE": "Cannot Sign In",
"SIGNED_IN_FAILED_MESSAGE": "Something went wrong while trying to sign in. Please try again.",
"SIGNED_OUT_FAILED_TITLE": "Failed to Sign Out",
"SIGNED_OUT_FAILED_MESSAGE": "Something went wrong while trying to sign out. Please try again.",
"SIGNED_OUT_FAILED_MESSAGE": "Something went wrong while logging out. Press OK to open <a href='https://account.phcode.dev/#advanced'>account.phcode.dev</a> where you can logout manually.",
"VALIDATION_CODE_TITLE": "Sign In Verification Code",
"VALIDATION_CODE_MESSAGE": "Please use this Verification code to sign in to your {APP_NAME} account:",
"COPY_VALIDATION_CODE": "Copy Code",
Expand All @@ -1607,6 +1607,15 @@ define({
"ACCOUNT_DETAILS": "Account Details",
"AI_QUOTA_USED": "AI quota used",
"LOGIN_REFRESH": "Check Login Status",
"SIGN_IN_WAITING_TITLE": "Waiting for Sign In",
"SIGN_IN_WAITING_MESSAGE": "Please complete sign-in in the new tab, then return here.",
"WAITING_FOR_LOGIN": "Waiting for login\u2026",
"CHECK_NOW": "Check Now",
"CHECKING": "Checking\u2026",
"CHECKING_STATUS": "Checking login status\u2026",
"NOT_SIGNED_IN_YET": "Not signed in yet. Please complete sign-in in the other tab.",
"WELCOME_BACK": "Welcome back, {0}!",
"POPUP_BLOCKED": "Pop-up blocked. Please allow pop-ups and try again, or manually navigate to {0}",

// Collapse Folders
"COLLAPSE_ALL_FOLDERS": "Collapse All Folders",
Expand Down
19 changes: 19 additions & 0 deletions src/services/html/browser-login-waiting-dialog.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<div class="browser-login-waiting-dialog modal">
<div class="modal-header">
<h1 class="dialog-title">{{Strings.SIGN_IN_WAITING_TITLE}}</h1>
</div>
<div class="modal-body">
<div class="waiting-content-container">
<p>{{Strings.SIGN_IN_WAITING_MESSAGE}}</p>
<div class="login-status-container" style="margin: 20px 0; text-align: center;">
<div id="login-status" style="color: #666; font-style: italic; font-size: 14px;">
{{Strings.WAITING_FOR_LOGIN}}
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn" data-button-id="cancel">{{Strings.CANCEL}}</button>
<button class="btn primary" data-button-id="check">{{Strings.CHECK_NOW}}</button>
</div>
</div>
Loading
Loading