Unverified 提交 41e66f70 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Fix socket firewall for windows (#3172)

<!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/3172" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end -->
上级 80c0281e
...@@ -365,6 +365,25 @@ describe("executeAddDependency", () => { ...@@ -365,6 +365,25 @@ describe("executeAddDependency", () => {
}); });
}); });
it("rejects invalid npm package specs before invoking the shell", async () => {
await expect(
executeAddDependency({
packages: ["react@^18.0.0"],
message: {
id: 1,
content:
'<dyad-add-dependency packages="react@^18.0.0"></dyad-add-dependency>',
} as any,
appPath: "/tmp/app",
}),
).rejects.toMatchObject({
displaySummary: "Invalid npm package name: react@^18.0.0",
warningMessages: [],
});
expect(runCommandMock).not.toHaveBeenCalled();
});
it("escapes package attributes and install output before storing the tag", async () => { it("escapes package attributes and install output before storing the tag", async () => {
ensureSocketFirewallInstalledMock.mockResolvedValue({ ensureSocketFirewallInstalledMock.mockResolvedValue({
available: false, available: false,
...@@ -376,18 +395,18 @@ describe("executeAddDependency", () => { ...@@ -376,18 +395,18 @@ describe("executeAddDependency", () => {
}); });
await executeAddDependency({ await executeAddDependency({
packages: ['react"&<safe>'], packages: ["react-safe"],
message: { message: {
id: 1, id: 1,
content: content:
'<dyad-add-dependency packages="react&quot;&amp;&lt;safe&gt;"></dyad-add-dependency>', '<dyad-add-dependency packages="react-safe"></dyad-add-dependency>',
} as any, } as any,
appPath: "/tmp/app", appPath: "/tmp/app",
}); });
expect(dbUpdateSetMock).toHaveBeenCalledWith({ expect(dbUpdateSetMock).toHaveBeenCalledWith({
content: content:
'<dyad-add-dependency packages="react&quot;&amp;&lt;safe&gt;">installed &lt;react&gt;</dyad-add-dependency>', '<dyad-add-dependency packages="react-safe">installed &lt;react&gt;</dyad-add-dependency>',
}); });
}); });
}); });
...@@ -3,6 +3,7 @@ import { messages } from "../../db/schema"; ...@@ -3,6 +3,7 @@ import { messages } from "../../db/schema";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { Message } from "@/ipc/types"; import { Message } from "@/ipc/types";
import { readEffectiveSettings } from "@/main/settings"; import { readEffectiveSettings } from "@/main/settings";
import { DyadError, DyadErrorKind } from "@/errors/dyad_error";
import { import {
ADD_DEPENDENCY_INSTALL_TIMEOUT_MS, ADD_DEPENDENCY_INSTALL_TIMEOUT_MS,
buildAddDependencyCommand, buildAddDependencyCommand,
...@@ -30,6 +31,8 @@ export interface ExecuteAddDependencyResult { ...@@ -30,6 +31,8 @@ export interface ExecuteAddDependencyResult {
warningMessages: string[]; warningMessages: string[];
} }
const NPM_PACKAGE_NAME_PATTERN = /^(@[a-z0-9-_.]+\/)?[a-z0-9-_.]+$/;
const DISPLAY_SUMMARY_PATTERNS = [ const DISPLAY_SUMMARY_PATTERNS = [
/\bblocked\b/i, /\bblocked\b/i,
/\bfailed\b/i, /\bfailed\b/i,
...@@ -161,6 +164,19 @@ export async function executeAddDependency({ ...@@ -161,6 +164,19 @@ export async function executeAddDependency({
message: Message; message: Message;
appPath: string; appPath: string;
}): Promise<ExecuteAddDependencyResult> { }): Promise<ExecuteAddDependencyResult> {
const invalidPackage = packages.find(
(pkg) => !NPM_PACKAGE_NAME_PATTERN.test(pkg),
);
if (invalidPackage) {
throw new ExecuteAddDependencyError({
error: new DyadError(
`Invalid npm package name: ${invalidPackage}`,
DyadErrorKind.Validation,
),
warningMessages: [],
});
}
const settings = await readEffectiveSettings(); const settings = await readEffectiveSettings();
const warningMessages: string[] = []; const warningMessages: string[] = [];
......
...@@ -185,9 +185,53 @@ describe("resolveExecutableName", () => { ...@@ -185,9 +185,53 @@ describe("resolveExecutableName", () => {
describe("buildPtyInvocation", () => { describe("buildPtyInvocation", () => {
it("wraps Windows .cmd shims through cmd.exe for PTY execution", () => { it("wraps Windows .cmd shims through cmd.exe for PTY execution", () => {
expect(buildPtyInvocation("npx", ["--yes", "sfw@2.0.4"], "win32")).toEqual({ expect(
buildPtyInvocation("npx", ["--yes", "sfw@2.0.4"], "win32", "cmd.exe"),
).toEqual({
command: "cmd.exe", command: "cmd.exe",
args: ["/d", "/s", "/c", '"npx.cmd" "--yes" "sfw@2.0.4"'], args: ["/d", "/s", "/c", "npx.cmd --yes sfw@2.0.4"],
});
});
it("quotes Windows arguments containing spaces and embedded quotes", () => {
expect(
buildPtyInvocation(
"npx",
["--message", 'value with spaces and "quotes"'],
"win32",
"cmd.exe",
),
).toEqual({
command: "cmd.exe",
args: [
"/d",
"/s",
"/c",
'npx.cmd --message "value with spaces and ""quotes"""',
],
});
});
it("quotes Windows arguments containing cmd metacharacters without mutating them", () => {
expect(
buildPtyInvocation(
"npx",
["--filter", "name&echo^(injected)"],
"win32",
"cmd.exe",
),
).toEqual({
command: "cmd.exe",
args: ["/d", "/s", "/c", 'npx.cmd --filter "name&echo^(injected)"'],
});
});
it("quotes empty Windows arguments so their position is preserved", () => {
expect(
buildPtyInvocation("npx", ["--flag", ""], "win32", "cmd.exe"),
).toEqual({
command: "cmd.exe",
args: ["/d", "/s", "/c", 'npx.cmd --flag ""'],
}); });
}); });
...@@ -220,7 +264,7 @@ describe("runCommand", () => { ...@@ -220,7 +264,7 @@ describe("runCommand", () => {
expect(runPtyCommandMock).toHaveBeenCalledWith( expect(runPtyCommandMock).toHaveBeenCalledWith(
"cmd.exe", "cmd.exe",
["/d", "/s", "/c", '"npx.cmd" "--yes" "sfw@2.0.4"'], ["/d", "/s", "/c", "npx.cmd --yes sfw@2.0.4"],
expect.objectContaining({ expect.objectContaining({
displayCommand: "npx --yes sfw@2.0.4", displayCommand: "npx --yes sfw@2.0.4",
}), }),
......
...@@ -13,6 +13,7 @@ const SOCKET_FIREWALL_NPX_ARGS = [ ...@@ -13,6 +13,7 @@ const SOCKET_FIREWALL_NPX_ARGS = [
SOCKET_FIREWALL_PACKAGE, SOCKET_FIREWALL_PACKAGE,
]; ];
const WINDOWS_BATCH_COMMAND_PATTERN = /\.(cmd|bat)$/i; const WINDOWS_BATCH_COMMAND_PATTERN = /\.(cmd|bat)$/i;
const WINDOWS_CMD_NEEDS_QUOTING_PATTERN = /[\s"&|<>^%!()]/u;
export const SOCKET_FIREWALL_PROBE_TIMEOUT_MS = 30 * 1000; export const SOCKET_FIREWALL_PROBE_TIMEOUT_MS = 30 * 1000;
export const PACKAGE_MANAGER_PROBE_TIMEOUT_MS = 30 * 1000; export const PACKAGE_MANAGER_PROBE_TIMEOUT_MS = 30 * 1000;
export const ADD_DEPENDENCY_INSTALL_TIMEOUT_MS = DEFAULT_PTY_COMMAND_TIMEOUT_MS; export const ADD_DEPENDENCY_INSTALL_TIMEOUT_MS = DEFAULT_PTY_COMMAND_TIMEOUT_MS;
...@@ -75,6 +76,12 @@ export function resolveExecutableName( ...@@ -75,6 +76,12 @@ export function resolveExecutableName(
} }
function quoteWindowsCmdArg(value: string): string { function quoteWindowsCmdArg(value: string): string {
// `cmd.exe /d /s /c` strips an outer quoted command string, so simple args
// stay unquoted while empty or shell-significant values are quoted/escaped.
if (value !== "" && !WINDOWS_CMD_NEEDS_QUOTING_PATTERN.test(value)) {
return value;
}
return `"${value.replace(/"/g, '""')}"`; return `"${value.replace(/"/g, '""')}"`;
} }
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论