Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make private key path OS indendant #2393

Merged
merged 9 commits into from
Dec 2, 2024
7 changes: 5 additions & 2 deletions src/api/IBMi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,10 @@ export default class IBMi {
});
const delayedOperations: Function[] = [...onConnectedOperations];

await this.client.connect(connectionObject as node_ssh.Config);
await this.client.connect({
...connectionObject,
privateKeyPath: connectionObject.privateKeyPath ? Tools.resolvePath(connectionObject.privateKeyPath) : undefined
} as node_ssh.Config);

cancelToken.onCancellationRequested(() => {
this.end();
Expand Down Expand Up @@ -1504,7 +1507,7 @@ export default class IBMi {
return this.componentManager.get<T>(name, ignoreState);
}

getComponentStates(){
getComponentStates() {
return this.componentManager.getState();
}

Expand Down
6 changes: 3 additions & 3 deletions src/api/IBMiContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -957,15 +957,15 @@ export default class IBMiContent {
target = localPath;
}

target = IBMi.escapeForShell(target);

let result: CommandResult;

if (assumeMember) {
target = IBMi.escapeForShell(target);
result = await this.ibmi.sendQsh({ command: `${this.ibmi.remoteFeatures.attr} -p ${target} ${operands.join(" ")}`});
} else {
target = Tools.escapePath(target, true);
// Take {DOES_THIS_WORK: `YESITDOES`} away, and all of a sudden names with # aren't found.
result = await this.ibmi.sendCommand({ command: `${this.ibmi.remoteFeatures.attr} -p ${target} ${operands.join(" ")}`, env: {DOES_THIS_WORK: `YESITDOES`}});
result = await this.ibmi.sendCommand({ command: `${this.ibmi.remoteFeatures.attr} -p "${target}" ${operands.join(" ")}`, env: {DOES_THIS_WORK: `YESITDOES`}});
}

if (result.code === 0) {
Expand Down
34 changes: 32 additions & 2 deletions src/api/Tools.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Crypto from 'crypto';
import { readFileSync } from "fs";
import os from "os";
import path from "path";
import vscode from "vscode";
import { IBMiMessage, IBMiMessages, QsysPath } from '../typings';
Expand Down Expand Up @@ -401,14 +402,14 @@ export namespace Tools {

export function assumeType(str: string) {
if (str.trim().length === 0) return ``;

// The number is already generated on the server.
// So, we assume that if the string starts with a 0, it is a string.
if (/^0.+/.test(str) || str.length > 10) {
return str
}
const number = Number(str);
if(isNaN(number)){
if (isNaN(number)) {
return str;
}
return number;
Expand Down Expand Up @@ -463,4 +464,33 @@ export namespace Tools {
}
return 0;
}

/**
* Transforms a file path into an OS agnostic path.
* - Replaces full home directory path by ~
* - Replaces all \ into / on Windows
*
* @param filePath
* @returns
*/
export function normalizePath(filePath: string) {
//Test path in lowercase since os.homedir doesn't always has the same case as filePath on Windows
if(filePath.toLowerCase().startsWith(os.homedir().toLowerCase())){
filePath = path.join(`~`, filePath.substring(os.homedir().length));
}

return process.platform === "win32" ? filePath.replaceAll('\\', '/') : filePath;
}

/**
* Transforms a normalized path into an OS specific path.
* - Replaces ~ with the current home directory
* - Changes all / to \ on Windows
* @param path
* @returns
*/
export function resolvePath(path: string) {
path = path.replace("~", os.homedir());
return process.platform === "win32" ? path.replaceAll('/', '\\') : path;
}
}
34 changes: 1 addition & 33 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { CustomUI } from "./api/CustomUI";
import { instance, loadAllofExtension } from './instantiate';

import { CompileTools } from "./api/CompileTools";
import { ConnectionConfiguration, ConnectionManager, GlobalConfiguration, onCodeForIBMiConfigurationChange } from "./api/Configuration";
import { ConnectionConfiguration, ConnectionManager, onCodeForIBMiConfigurationChange } from "./api/Configuration";
import IBMi from "./api/IBMi";
import { GlobalStorage } from "./api/Storage";
import { Tools } from "./api/Tools";
Expand Down Expand Up @@ -142,38 +142,6 @@ export async function activate(context: ExtensionContext): Promise<CodeForIBMi>
};
}

async function fixLoginSettings() {
const connections = (GlobalConfiguration.get<ConnectionData[]>(`connections`) || []);
let update = false;
for (const connection of connections) {
//privateKey was used to hold privateKeyPath
if ('privateKey' in connection) {
const privateKey = connection["privateKey"] as string;
if (privateKey) {
connection.privateKeyPath = privateKey;
}
delete connection["privateKey"];
update = true;
}

//An empty privateKeyPath will crash the connection
if (!connection.privateKeyPath?.trim()) {
connection.privateKeyPath = undefined;
update = true;
}

//buttons were added by the login settings page
if (`buttons` in connection) {
delete connection["buttons"];
update = true;
}
}

if (update) {
await GlobalConfiguration.set(`connections`, connections);
}
}

// this method is called when your extension is deactivated
export async function deactivate() {
await commands.executeCommand(`code-for-ibmi.disconnect`, true);
Expand Down
38 changes: 38 additions & 0 deletions src/testing/encoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,44 @@ export const EncodingSuite: TestSuite = {
assert.ok(result?.length);
}
},
{
name: `Files and directories with spaces`, test: async () => {
const connection = instance.getConnection()!;

await connection.withTempDirectory(async tempDir => {
const dirName = `hello world`;
const dirWithSpace = path.posix.join(tempDir, dirName);
const fileName = `hello world.txt`;
const nameWithSpace = path.posix.join(dirWithSpace, fileName);

await connection.sendCommand({command: `mkdir -p "${dirWithSpace}"`});
await connection.content.createStreamFile(nameWithSpace);

// Resolve and get attributes
const resolved = await connection.content.streamfileResolve([fileName], [tempDir, dirWithSpace]);
assert.strictEqual(resolved, nameWithSpace);

const attributes = await connection.content.getAttributes(resolved, `CCSID`);
assert.ok(attributes);

// Write and read the files
const uri = Uri.from({scheme: `streamfile`, path: nameWithSpace});
await workspace.fs.writeFile(uri, Buffer.from(`Hello world`, `utf8`));

const streamfileContents = await workspace.fs.readFile(uri);
assert.ok(streamfileContents.toString().includes(`Hello world`));

// List files
const files = await connection.content.getFileList(tempDir);
assert.strictEqual(files.length, 1);
assert.ok(files.some(f => f.name === dirName && f.path === dirWithSpace));

const files2 = await connection.content.getFileList(dirWithSpace);
assert.strictEqual(files2.length, 1);
assert.ok(files2.some(f => f.name === fileName && f.path === nameWithSpace));
});
}
},
{
name: `Run variants through shells`, test: async () => {
const connection = instance.getConnection();
Expand Down
1 change: 0 additions & 1 deletion src/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ export interface ConnectionData {
port: number;
username: string;
password?: string;
privateKey?: string;
privateKeyPath?: string;
keepaliveInterval?: number;
}
Expand Down
3 changes: 2 additions & 1 deletion src/webviews/login/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import vscode, { l10n, ThemeIcon } from "vscode";
import { ConnectionConfiguration, ConnectionManager } from "../../api/Configuration";
import { CustomUI, Section } from "../../api/CustomUI";
import IBMi from "../../api/IBMi";
import { Tools } from "../../api/Tools";
import { disconnect, instance } from "../../instantiate";
import { ConnectionData } from '../../typings';

Expand Down Expand Up @@ -54,7 +55,7 @@ export class Login {
page.panel.dispose();

data.port = Number(data.port);
data.privateKeyPath = data.privateKeyPath?.trim() ? data.privateKeyPath : undefined;
data.privateKeyPath = data.privateKeyPath?.trim() ? Tools.normalizePath(data.privateKeyPath) : undefined;
if (data.name) {
const existingConnection = ConnectionManager.getByName(data.name);

Expand Down
7 changes: 5 additions & 2 deletions src/webviews/settings/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { existsSync } from "fs";
import vscode from "vscode";
import { ConnectionConfiguration, ConnectionManager, GlobalConfiguration } from "../../api/Configuration";
import { ComplexTab, CustomUI, Section } from "../../api/CustomUI";
Expand Down Expand Up @@ -352,14 +353,15 @@ export class SettingsUI {
if (connection) {
const storedPassword = await ConnectionManager.getStoredPassword(context, name);
let { data: stored, index } = connection;

const privateKeyPath = stored.privateKeyPath ? Tools.resolvePath(stored.privateKeyPath) : undefined;
const privateKeyWarning = !privateKeyPath || existsSync(privateKeyPath) ? "" : "<b>⚠️ This private key doesn't exist on this system! ⚠️</b></br></br>";
const ui = new CustomUI()
.addInput(`host`, vscode.l10n.t(`Host or IP Address`), undefined, { default: stored.host, minlength: 1 })
.addInput(`port`, vscode.l10n.t(`Port (SSH)`), undefined, { default: String(stored.port), minlength: 1, maxlength: 5, regexTest: `^\\d+$` })
.addInput(`username`, vscode.l10n.t(`Username`), undefined, { default: stored.username, minlength: 1 })
.addParagraph(vscode.l10n.t(`Only provide either the password or a private key - not both.`))
.addPassword(`password`, `${vscode.l10n.t(`Password`)}${storedPassword ? ` (${vscode.l10n.t(`stored`)})` : ``}`, vscode.l10n.t("Only provide a password if you want to update an existing one or set a new one."))
.addFile(`privateKeyPath`, `${vscode.l10n.t(`Private Key`)}${stored.privateKeyPath ? ` (${vscode.l10n.t(`Private Key`)}: ${stored.privateKeyPath})` : ``}`, vscode.l10n.t("Only provide a private key if you want to update from the existing one or set one.") + '<br />' + vscode.l10n.t("OpenSSH, RFC4716 and PPK formats are supported."))
.addFile(`privateKeyPath`, `${vscode.l10n.t(`Private Key`)}${privateKeyPath ? ` (${vscode.l10n.t(`Private Key`)}: ${privateKeyPath})` : ``}`, privateKeyWarning + vscode.l10n.t("Only provide a private key if you want to update from the existing one or set one.") + '<br />' + vscode.l10n.t("OpenSSH, RFC4716 and PPK formats are supported."))
.addButtons(
{ id: `submitButton`, label: vscode.l10n.t(`Save`), requiresValidation: true },
{ id: `removeAuth`, label: vscode.l10n.t(`Remove auth methods`) }
Expand Down Expand Up @@ -393,6 +395,7 @@ export class SettingsUI {
// If no password was entered, but a keypath exists
// then remove the password from the data and
// use the keypath instead
data.privateKeyPath = Tools.normalizePath(data.privateKeyPath);
await ConnectionManager.deleteStoredPassword(context, name);
vscode.window.showInformationMessage(vscode.l10n.t(`Private key updated and will be used for "{0}".`, name));
}
Expand Down
Loading