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
1 change: 1 addition & 0 deletions .vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@
!assets/walkthrough/**
!assets/documentation-webview/**
!assets/swift-docc-render/**
!assets/swift_askpass.sh
!node_modules/@vscode/codicons/**
10 changes: 10 additions & 0 deletions assets/swift_askpass.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/sh
VSCODE_SWIFT_ASKPASS_FILE=$(mktemp)

ELECTRON_RUN_AS_NODE="1" VSCODE_SWIFT_ASKPASS_FILE="$VSCODE_SWIFT_ASKPASS_FILE" "$VSCODE_SWIFT_ASKPASS_NODE" "$VSCODE_SWIFT_ASKPASS_MAIN"
EXIT_CODE=$?

cat "$VSCODE_SWIFT_ASKPASS_FILE"
rm "$VSCODE_SWIFT_ASKPASS_FILE"

exit "$EXIT_CODE"
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1991,7 +1991,7 @@
"scripts": {
"vscode:prepublish": "npm run bundle",
"bundle": "del-cli ./dist && npm run bundle-extension && npm run bundle-documentation-webview",
"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",
"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",
"bundle-documentation-webview": "npm run compile-documentation-webview -- --minify",
"compile": "del-cli ./dist/ && tsc --build",
"watch": "npm run compile -- --watch",
Expand Down
10 changes: 8 additions & 2 deletions src/FolderContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,10 @@ export class FolderContext implements vscode.Disposable {

let toolchain: SwiftToolchain;
try {
toolchain = await SwiftToolchain.create(folder);
toolchain = await SwiftToolchain.create(
workspaceContext.extensionContext.extensionPath,
folder
);
} catch (error) {
// This error case is quite hard for the user to get in to, but possible.
// Typically on startup the toolchain creation failure is going to happen in
Expand All @@ -108,7 +111,10 @@ export class FolderContext implements vscode.Disposable {
if (userMadeSelection) {
// User updated toolchain settings, retry once
try {
toolchain = await SwiftToolchain.create(folder);
toolchain = await SwiftToolchain.create(
workspaceContext.extensionContext.extensionPath,
folder
);
workspaceContext.logger.info(
`Successfully created toolchain for ${FolderContext.uriName(folder)} after user selection`,
FolderContext.uriName(folder)
Expand Down
2 changes: 1 addition & 1 deletion src/WorkspaceContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export class WorkspaceContext implements vscode.Disposable {
public loggerFactory: SwiftLoggerFactory;

constructor(
extensionContext: vscode.ExtensionContext,
public extensionContext: vscode.ExtensionContext,
public contextKeys: ContextKeys,
public logger: SwiftLogger,
public globalToolchain: SwiftToolchain
Expand Down
82 changes: 82 additions & 0 deletions src/askpass/askpass-main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the VS Code Swift open source project
//
// Copyright (c) 2025 the VS Code Swift project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
/* eslint-disable no-console */
import * as fs from "fs";
import * as http from "http";
import { z } from "zod/v4/mini";

const outputFile = process.env.VSCODE_SWIFT_ASKPASS_FILE;
if (!outputFile) {
throw new Error("Missing environment variable $VSCODE_SWIFT_ASKPASS_FILE");
}

const nonce = process.env.VSCODE_SWIFT_ASKPASS_NONCE;
if (!nonce) {
throw new Error("Missing environment variable $VSCODE_SWIFT_ASKPASS_NONCE");
}

const port = Number.parseInt(process.env.VSCODE_SWIFT_ASKPASS_PORT ?? "-1", 10);
if (isNaN(port) || port < 0) {
throw new Error("Missing environment variable $VSCODE_SWIFT_ASKPASS_PORT");
}

const req = http.request(
{
hostname: "localhost",
port: port,
path: `/askpass?nonce=${encodeURIComponent(nonce)}`,
method: "GET",
},
res => {
function parseResponse(rawData: string): { password?: string } {
try {
const rawJSON = JSON.parse(rawData);
return z.object({ password: z.optional(z.string()) }).parse(rawJSON);
} catch {
// DO NOT log the underlying error here. It contains sensitive password info!
throw Error("Failed to parse response from askpass server.");
}
}

let rawData = "";
res.on("data", chunk => {
rawData += chunk;
});

res.on("end", () => {
if (res.statusCode !== 200) {
console.error(`Server responded with status code ${res.statusCode}`);
process.exit(1);
}
const password = parseResponse(rawData).password;
if (!password) {
console.error("User cancelled password input.");
process.exit(1);
}
try {
fs.writeFileSync(outputFile, password, "utf8");
} catch (error) {
console.error(Error(`Unable to write to file ${outputFile}`, { cause: error }));
process.exit(1);
}
});
}
);

req.on("error", error => {
console.error(Error(`Request failed: GET ${req.host}/${req.path}`, { cause: error }));
process.exit(1);
});

req.end();
88 changes: 88 additions & 0 deletions src/askpass/askpass-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the VS Code Swift open source project
//
// Copyright (c) 2025 the VS Code Swift project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import * as crypto from "crypto";
import * as http from "http";
import * as vscode from "vscode";

/** Options that can be used to configure the behavior of {@link withAskpassServer}. */
export interface WithAskpassServerOptions {
/** The title of the input box shown in VS Code. */
title?: string;
}

/**
* Creates a temporary HTTP server that can be used to handle askpass requests from various terminal
* applications. The server will be closed when the provided task completes.
*
* The task will be provided with a randomly generated nonce and port number used for connecting to
* the server. Requests without a valid nonce will be rejected with a 401 status code.
*
* @param task Function to execute while the server is listening for connections
* @returns Promise that resolves when the task completes and server is cleaned up
*/
export async function withAskpassServer<T>(
task: (nonce: string, port: number) => Promise<T>,
options: WithAskpassServerOptions = {}
): Promise<T> {
const nonce = crypto.randomBytes(32).toString("hex");
const server = http.createServer((req, res) => {
if (!req.url) {
return res.writeHead(404).end();
}

const url = new URL(req.url, `http://localhost`);
if (url.pathname !== "/askpass") {
return res.writeHead(404).end();
}

const requestNonce = url.searchParams.get("nonce");
if (requestNonce !== nonce) {
return res.writeHead(401).end();
}

void vscode.window
.showInputBox({
password: true,
title: options.title,
placeHolder: "Please enter your password",
ignoreFocusOut: true,
})
.then(password => {
res.writeHead(200, { "Content-Type": "application/json" }).end(
JSON.stringify({ password })
);
});
});

return new Promise((resolve, reject) => {
server.listen(0, "localhost", async () => {
try {
const address = server.address();
if (!address || typeof address === "string") {
throw new Error("Failed to get server port");
}
const port = address.port;
resolve(await task(nonce, port));
} catch (error) {
reject(error);
} finally {
server.close();
}
});

server.on("error", error => {
reject(error);
});
});
}
3 changes: 3 additions & 0 deletions src/commands/installSwiftlyToolchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
*/
export async function installSwiftlyToolchainWithProgress(
version: string,
extensionRoot: string,
logger?: SwiftLogger
): Promise<boolean> {
try {
Expand All @@ -48,6 +49,7 @@ export async function installSwiftlyToolchainWithProgress(

await Swiftly.installToolchain(
version,
extensionRoot,
(progressData: SwiftlyProgressData) => {
if (progressData.complete) {
// Swiftly will also verify the signature and extract the toolchain after the
Expand Down Expand Up @@ -185,6 +187,7 @@ export async function promptToInstallSwiftlyToolchain(
if (
!(await installSwiftlyToolchainWithProgress(
selectedToolchain.toolchain.version.name,
ctx.extensionContext.extensionPath,
ctx.logger
))
) {
Expand Down
5 changes: 3 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<Api> {
checkAndWarnAboutWindowsSymlinks(logger);

const contextKeys = createContextKeys();
const toolchain = await createActiveToolchain(contextKeys, logger);
const toolchain = await createActiveToolchain(context, contextKeys, logger);
checkForSwiftlyInstallation(contextKeys, logger);

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

async function createActiveToolchain(
extension: vscode.ExtensionContext,
contextKeys: ContextKeys,
logger: SwiftLogger
): Promise<SwiftToolchain | undefined> {
try {
const toolchain = await SwiftToolchain.create(undefined, logger);
const toolchain = await SwiftToolchain.create(extension.extensionPath, undefined, logger);
toolchain.logDiagnostics(logger);
contextKeys.updateKeysBasedOnActiveVersion(toolchain.swiftVersion);
return toolchain;
Expand Down
Loading