Skip to content

Commit d4fc2ae

Browse files
use sudo with the askpass option instead of pkexec
1 parent 117501c commit d4fc2ae

File tree

19 files changed

+450
-61
lines changed

19 files changed

+450
-61
lines changed

.vscodeignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@
1212
!assets/walkthrough/**
1313
!assets/documentation-webview/**
1414
!assets/swift-docc-render/**
15+
!assets/swift_askpass.sh
1516
!node_modules/@vscode/codicons/**

assets/swift_askpass.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/bin/sh
2+
VSCODE_SWIFT_ASKPASS_FILE=$(mktemp)
3+
4+
ELECTRON_RUN_AS_NODE="1" VSCODE_SWIFT_ASKPASS_FILE="$VSCODE_SWIFT_ASKPASS_FILE" "$VSCODE_SWIFT_ASKPASS_NODE" "$VSCODE_SWIFT_ASKPASS_MAIN"
5+
EXIT_CODE=$?
6+
7+
cat "$VSCODE_SWIFT_ASKPASS_FILE"
8+
rm "$VSCODE_SWIFT_ASKPASS_FILE"
9+
10+
exit "$EXIT_CODE"

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1991,7 +1991,7 @@
19911991
"scripts": {
19921992
"vscode:prepublish": "npm run bundle",
19931993
"bundle": "del-cli ./dist && npm run bundle-extension && npm run bundle-documentation-webview",
1994-
"bundle-extension": "del-cli ./dist && esbuild ./src/extension.ts --bundle --outfile=dist/src/extension.js --external:vscode --define:process.env.NODE_ENV=\\\"production\\\" --define:process.env.CI=\\\"\\\" --format=cjs --platform=node --target=node18 --minify --sourcemap",
1994+
"bundle-extension": "del-cli ./dist && esbuild ./src/extension.ts ./src/askpass/askpass-main.ts --bundle --outdir=dist/src/ --external:vscode --define:process.env.NODE_ENV=\\\"production\\\" --define:process.env.CI=\\\"\\\" --format=cjs --platform=node --target=node18 --minify --sourcemap",
19951995
"bundle-documentation-webview": "npm run compile-documentation-webview -- --minify",
19961996
"compile": "del-cli ./dist/ && tsc --build",
19971997
"watch": "npm run compile -- --watch",

src/FolderContext.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ export class FolderContext implements vscode.Disposable {
9494

9595
let toolchain: SwiftToolchain;
9696
try {
97-
toolchain = await SwiftToolchain.create(folder);
97+
toolchain = await SwiftToolchain.create(
98+
workspaceContext.extensionContext.extensionPath,
99+
folder
100+
);
98101
} catch (error) {
99102
// This error case is quite hard for the user to get in to, but possible.
100103
// Typically on startup the toolchain creation failure is going to happen in
@@ -108,7 +111,10 @@ export class FolderContext implements vscode.Disposable {
108111
if (userMadeSelection) {
109112
// User updated toolchain settings, retry once
110113
try {
111-
toolchain = await SwiftToolchain.create(folder);
114+
toolchain = await SwiftToolchain.create(
115+
workspaceContext.extensionContext.extensionPath,
116+
folder
117+
);
112118
workspaceContext.logger.info(
113119
`Successfully created toolchain for ${FolderContext.uriName(folder)} after user selection`,
114120
FolderContext.uriName(folder)

src/WorkspaceContext.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export class WorkspaceContext implements vscode.Disposable {
8282
public loggerFactory: SwiftLoggerFactory;
8383

8484
constructor(
85-
extensionContext: vscode.ExtensionContext,
85+
public extensionContext: vscode.ExtensionContext,
8686
public contextKeys: ContextKeys,
8787
public logger: SwiftLogger,
8888
public globalToolchain: SwiftToolchain

src/askpass/askpass-main.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2025 the VS Code Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
/* eslint-disable no-console */
15+
import * as fs from "fs";
16+
import * as http from "http";
17+
import { z } from "zod/v4/mini";
18+
19+
const outputFile = process.env.VSCODE_SWIFT_ASKPASS_FILE;
20+
if (!outputFile) {
21+
throw new Error("Missing environment variable $VSCODE_SWIFT_ASKPASS_FILE");
22+
}
23+
24+
const nonce = process.env.VSCODE_SWIFT_ASKPASS_NONCE;
25+
if (!nonce) {
26+
throw new Error("Missing environment variable $VSCODE_SWIFT_ASKPASS_NONCE");
27+
}
28+
29+
const port = Number.parseInt(process.env.VSCODE_SWIFT_ASKPASS_PORT ?? "-1", 10);
30+
if (isNaN(port) || port < 0) {
31+
throw new Error("Missing environment variable $VSCODE_SWIFT_ASKPASS_PORT");
32+
}
33+
34+
const req = http.request(
35+
{
36+
hostname: "localhost",
37+
port: port,
38+
path: `/askpass?nonce=${encodeURIComponent(nonce)}`,
39+
method: "GET",
40+
},
41+
res => {
42+
function parseResponse(rawData: string): { password?: string } {
43+
try {
44+
const rawJSON = JSON.parse(rawData);
45+
return z.object({ password: z.optional(z.string()) }).parse(rawJSON);
46+
} catch {
47+
// DO NOT log the underlying error here. It contains sensitive password info!
48+
throw Error("Failed to parse response from askpass server.");
49+
}
50+
}
51+
52+
let rawData = "";
53+
res.on("data", chunk => {
54+
rawData += chunk;
55+
});
56+
57+
res.on("end", () => {
58+
if (res.statusCode !== 200) {
59+
console.error(`Server responded with status code ${res.statusCode}`);
60+
process.exit(1);
61+
}
62+
const password = parseResponse(rawData).password;
63+
if (!password) {
64+
console.error("User cancelled password input.");
65+
process.exit(1);
66+
}
67+
try {
68+
fs.writeFileSync(outputFile, password, "utf8");
69+
} catch (error) {
70+
console.error(Error(`Unable to write to file ${outputFile}`, { cause: error }));
71+
process.exit(1);
72+
}
73+
});
74+
}
75+
);
76+
77+
req.on("error", error => {
78+
console.error(Error(`Request failed: GET ${req.host}/${req.path}`, { cause: error }));
79+
process.exit(1);
80+
});
81+
82+
req.end();

src/askpass/askpass-server.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2025 the VS Code Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
import * as crypto from "crypto";
15+
import * as http from "http";
16+
import * as vscode from "vscode";
17+
18+
/** Options that can be used to configure the behavior of {@link withAskpassServer}. */
19+
export interface WithAskpassServerOptions {
20+
/** The title of the input box shown in VS Code. */
21+
title?: string;
22+
}
23+
24+
/**
25+
* Creates a temporary HTTP server that can be used to handle askpass requests from various terminal
26+
* applications. The server will be closed when the provided task completes.
27+
*
28+
* The task will be provided with a randomly generated nonce and port number used for connecting to
29+
* the server. Requests without a valid nonce will be rejected with a 401 status code.
30+
*
31+
* @param task Function to execute while the server is listening for connections
32+
* @returns Promise that resolves when the task completes and server is cleaned up
33+
*/
34+
export async function withAskpassServer<T>(
35+
task: (nonce: string, port: number) => Promise<T>,
36+
options: WithAskpassServerOptions = {}
37+
): Promise<T> {
38+
const nonce = crypto.randomBytes(32).toString("hex");
39+
const server = http.createServer((req, res) => {
40+
if (!req.url) {
41+
return res.writeHead(404).end();
42+
}
43+
44+
const url = new URL(req.url, `http://localhost`);
45+
if (url.pathname !== "/askpass") {
46+
return res.writeHead(404).end();
47+
}
48+
49+
const requestNonce = url.searchParams.get("nonce");
50+
if (requestNonce !== nonce) {
51+
return res.writeHead(401).end();
52+
}
53+
54+
void vscode.window
55+
.showInputBox({
56+
password: true,
57+
title: options.title,
58+
placeHolder: "Please enter your password",
59+
ignoreFocusOut: true,
60+
})
61+
.then(password => {
62+
res.writeHead(200, { "Content-Type": "application/json" }).end(
63+
JSON.stringify({ password })
64+
);
65+
});
66+
});
67+
68+
return new Promise((resolve, reject) => {
69+
server.listen(0, "localhost", async () => {
70+
try {
71+
const address = server.address();
72+
if (!address || typeof address === "string") {
73+
throw new Error("Failed to get server port");
74+
}
75+
const port = address.port;
76+
resolve(await task(nonce, port));
77+
} catch (error) {
78+
reject(error);
79+
} finally {
80+
server.close();
81+
}
82+
});
83+
84+
server.on("error", error => {
85+
reject(error);
86+
});
87+
});
88+
}

src/commands/installSwiftlyToolchain.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
*/
3333
export async function installSwiftlyToolchainWithProgress(
3434
version: string,
35+
extensionRoot: string,
3536
logger?: SwiftLogger
3637
): Promise<boolean> {
3738
try {
@@ -48,6 +49,7 @@ export async function installSwiftlyToolchainWithProgress(
4849

4950
await Swiftly.installToolchain(
5051
version,
52+
extensionRoot,
5153
(progressData: SwiftlyProgressData) => {
5254
if (progressData.complete) {
5355
// Swiftly will also verify the signature and extract the toolchain after the
@@ -185,6 +187,7 @@ export async function promptToInstallSwiftlyToolchain(
185187
if (
186188
!(await installSwiftlyToolchainWithProgress(
187189
selectedToolchain.toolchain.version.name,
190+
ctx.extensionContext.extensionPath,
188191
ctx.logger
189192
))
190193
) {

src/extension.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<Api> {
6868
checkAndWarnAboutWindowsSymlinks(logger);
6969

7070
const contextKeys = createContextKeys();
71-
const toolchain = await createActiveToolchain(contextKeys, logger);
71+
const toolchain = await createActiveToolchain(context, contextKeys, logger);
7272
checkForSwiftlyInstallation(contextKeys, logger);
7373

7474
// If we don't have a toolchain, show an error and stop initializing the extension.
@@ -248,11 +248,12 @@ function handleFolderEvent(logger: SwiftLogger): (event: FolderEvent) => Promise
248248
}
249249

250250
async function createActiveToolchain(
251+
extension: vscode.ExtensionContext,
251252
contextKeys: ContextKeys,
252253
logger: SwiftLogger
253254
): Promise<SwiftToolchain | undefined> {
254255
try {
255-
const toolchain = await SwiftToolchain.create(undefined, logger);
256+
const toolchain = await SwiftToolchain.create(extension.extensionPath, undefined, logger);
256257
toolchain.logDiagnostics(logger);
257258
contextKeys.updateKeysBasedOnActiveVersion(toolchain.swiftVersion);
258259
return toolchain;

0 commit comments

Comments
 (0)