Allow using PuTTY sessions

pull/13/head
Kelvin Schoofs 7 years ago
parent fe9b6b5041
commit a58b62907d

@ -3,6 +3,12 @@
This extension makes use of the new FileSystemProvider, added in version 1.23.0 of Visual Studio Code. This extension makes use of the new FileSystemProvider, added in version 1.23.0 of Visual Studio Code.
## Features
* Use a remote directory (over SSH) as workspace folder
* Use agents, including Pageant for Windows
* Easily create configurations that mirror a PuTTY session
* Have multiple SSH workspace folders at once
## Usage ## Usage
Add SSH FS configs to "sshfs.configs" in your User Settings: Add SSH FS configs to "sshfs.configs" in your User Settings:
```js ```js
@ -20,17 +26,52 @@ Add SSH FS configs to "sshfs.configs" in your User Settings:
// Username to login with // Username to login with
"username": "root", "username": "root",
// Path to ssh-agent's UNIX socket
// Path to ssh-agent's UNIX socket (cygwin ones should work too)
// or 'pageant' when using Pageant on Windows // or 'pageant' when using Pageant on Windows
"agent": "pageant" "agent": "pageant"
// Instead of using an agent, we can also just use a password // Instead of using an agent, we can also just use a password
"password": "CorrectHorseBatteryStaple" "password": "CorrectHorseBatteryStaple"
// Or a private key (raw key, OpenSSH format) // Or a private key (raw key, OpenSSH format)
// (can also be a public key for host-based authentication)
"privateKey": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnN...", "privateKey": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnN...",
// Should the private key be encrypted // Should the private key be encrypted
"passphrase": "CorrectHorseBatteryStaple" "passphrase": "CorrectHorseBatteryStaple"
},
{
// If you're on Windows and have PuTTY installed
"name": "media-server",
"root": "/data/media/",
// Either set this to a session name
"putty": "My media server",
// Or let it find one using the host (and username)
"putty": true,
// Can also be a session name, e.g. "My media server"
"host": "my.media.me",
// (Optional) Filter the session by username
// (This only works if host is NOT a name of a session)
"username": "media",
// If the session has an encrypted key
"passphrase": "CorrectHorseBatteryStaple"
// Note: If the session doesn't specify a username, but
// has "Use system username" enabled, it'll use process.env.USER
// Note: The "agent" option will be set to "pageant" if the
// session has "Attempt authentication using Pageant" set
},
{
// With PuTTY, this can be a complete configuration (with / as root)
"name": "quick-putty",
"putty": "My PuTTY session"
} }
], ],
} }
@ -49,8 +90,11 @@ This will add a Workspace folder linked to a SSH (SFTP) session:
## TO DO *(in order of most likely to implement first)* ## TO DO *(in order of most likely to implement first)*
* ~~Fix bug where saving a file resets the permissions (when owner/root at least)~~ **DONE** * ~~Fix bug where saving a file resets the permissions (when owner/root at least)~~ **DONE**
* ~~Allow loading PuTTY sessions when on windows~~ **DONE**
* Also have a command to directly use a PuTTY session (**TODO**)
* Fix bug where the Explorer shows a loading bar forever * Fix bug where the Explorer shows a loading bar forever
* Fix bug where VSCode shows an error message about `no provider for ssh://NAME/` * Fix bug where VSCode shows an error message about `no provider for ssh://NAME/`
* Allow loading (or automatically use) sessions from .ssh/config
* An icon for the extension * An icon for the extension
* Configuring a deleted (but active) configuration should show the old config * Configuring a deleted (but active) configuration should show the old config
* Currently it'll open a new default configuration file for it * Currently it'll open a new default configuration file for it

13
package-lock.json generated

@ -1,6 +1,6 @@
{ {
"name": "vscode-sshfs", "name": "vscode-sshfs",
"version": "0.0.1", "version": "1.0.2",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -29,6 +29,12 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/winreg": {
"version": "1.2.30",
"resolved": "https://registry.npmjs.org/@types/winreg/-/winreg-1.2.30.tgz",
"integrity": "sha1-kdZxDlNtNFucmwF8V0z2qNpkxRg=",
"dev": true
},
"ajv": { "ajv": {
"version": "5.5.2", "version": "5.5.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
@ -2423,6 +2429,11 @@
"vinyl-source-stream": "^1.1.0" "vinyl-source-stream": "^1.1.0"
} }
}, },
"winreg": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/winreg/-/winreg-1.2.4.tgz",
"integrity": "sha1-ugZWKbepJRMOFXeRCM9UCZDpjRs="
},
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",

@ -208,6 +208,10 @@
"passphrase": { "passphrase": {
"type": "string", "type": "string",
"description": "For an encrypted private key, this is the passphrase used to decrypt it" "description": "For an encrypted private key, this is the passphrase used to decrypt it"
},
"putty": {
"type": ["string", "boolean"],
"description": "(Windows only) Use the settings from the given PuTTY session (set this to true to find one using the other settings)"
} }
} }
} }
@ -224,10 +228,12 @@
"devDependencies": { "devDependencies": {
"@types/node": "^7.0.43", "@types/node": "^7.0.43",
"@types/ssh2": "^0.5.35", "@types/ssh2": "^0.5.35",
"@types/winreg": "^1.2.30",
"typescript": "^2.5.2", "typescript": "^2.5.2",
"vscode": "^1.1.17" "vscode": "^1.1.17"
}, },
"dependencies": { "dependencies": {
"ssh2": "^0.6.0" "ssh2": "^0.6.0",
"winreg": "^1.2.4"
} }
} }

@ -1,6 +1,9 @@
import { readFile } from 'fs';
import { Client, ConnectConfig } from 'ssh2'; import { Client, ConnectConfig } from 'ssh2';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { getSession as getPuttySession, PuttySession } from './putty';
import SSHFileSystem, { EMPTY_FILE_SYSTEM } from './sshFileSystem'; import SSHFileSystem, { EMPTY_FILE_SYSTEM } from './sshFileSystem';
import { toPromise } from './toPromise'; import { toPromise } from './toPromise';
@ -14,6 +17,7 @@ async function assertFs(man: Manager, uri: vscode.Uri) {
export interface FileSystemConfig extends ConnectConfig { export interface FileSystemConfig extends ConnectConfig {
name: string; name: string;
root?: string; root?: string;
putty?: string | boolean;
} }
function createTreeItem(manager: Manager, name: string): vscode.TreeItem { function createTreeItem(manager: Manager, name: string): vscode.TreeItem {
@ -126,11 +130,38 @@ export class Manager implements vscode.FileSystemProvider, vscode.TreeDataProvid
if (promise) return promise; if (promise) return promise;
// config = config || this.memento.get(`fs.config.${name}`); // config = config || this.memento.get(`fs.config.${name}`);
config = config || (await this.loadConfigs()).find(c => c.name === name); config = config || (await this.loadConfigs()).find(c => c.name === name);
promise = new Promise<SSHFileSystem>((resolve, reject) => { promise = new Promise<SSHFileSystem>(async (resolve, reject) => {
if (!config) { if (!config) {
throw new Error(`A SSH filesystem with the name '${name}' doesn't exist`); throw new Error(`A SSH filesystem with the name '${name}' doesn't exist`);
} }
this.registerFileSystem(name, config); this.registerFileSystem(name, config);
if (config.putty) {
let nameOnly = true;
if (config.putty === true) {
if (!config.host) return reject(new Error(`'putty' was true but 'host' is empty/missing`));
config.putty = config.host;
nameOnly = false;
}
const session = await getPuttySession(config.putty, config.host, config.username, nameOnly);
if (!session) return reject(new Error(`Couldn't find the requested PuTTY session`));
if (session.protocol !== 'ssh') return reject(new Error(`The requested PuTTY session isn't a SSH session`));
config.username = session.username;
config.host = session.hostname;
config.port = session.portnumber;
config.agent = session.tryagent ? 'pageant' : undefined;
if (session.usernamefromenvironment) {
session.username = process.env.USERNAME;
if (!session.username) return reject(new Error(`No username specified in the session (nor is using the system username enabled)`));
}
if (session.publickeyfile) {
try {
const key = await toPromise<Buffer>(cb => readFile(session.publickeyfile, cb));
config.privateKey = key;
} catch (e) {
return reject(new Error(`Error while reading the keyfile at:\n${session.publickeyfile}`));
}
}
}
const client = new Client(); const client = new Client();
client.on('ready', () => { client.on('ready', () => {
client.sftp((err, sftp) => { client.sftp((err, sftp) => {

@ -0,0 +1,61 @@
import * as Winreg from 'winreg';
import { toPromise } from './toPromise';
const winreg = new Winreg({
hive: Winreg.HKCU,
key: `\\Software\\SimonTatham\\PuTTY\\Sessions\\`,
});
export type NumberAsBoolean = 0 | 1;
export interface PuttySession {
[key: string]: string | number;
name: string;
hostname: string;
protocol: string;
portnumber: number;
username: string;
usernamefromenvironment: NumberAsBoolean;
tryagent: NumberAsBoolean;
publickeyfile: string;
}
function valueFromItem(item: Winreg.RegistryItem) {
switch (item.type) {
case 'REG_DWORD':
return parseInt(item.value, 16);
case 'REG_SZ':
return item.value;
}
console.log(item.name, item.value);
throw new Error(`Unknown RegistryItem type: '${item.type}'`);
}
export async function getSessions() {
const values = await toPromise<Winreg.Registry[]>(cb => winreg.keys(cb));
const sessions: PuttySession[] = [];
await Promise.all(values.map(regSession => (async (res, rej) => {
const name = decodeURIComponent(regSession.key.substr(winreg.key.length));
const props = await toPromise<Winreg.RegistryItem[]>(cb => regSession.values(cb));
const properties: { [key: string]: string | number } = {};
props.forEach(prop => properties[prop.name.toLowerCase()] = valueFromItem(prop));
sessions.push({ name, ...(properties as any) });
})()));
return sessions;
}
export async function getSession(name?: string, host?: string, username?: string, nameOnly = false): Promise<PuttySession | null> {
const sessions = await getSessions();
if (name) {
name = name.toLowerCase();
const session = sessions.find(s => s.name.toLowerCase() === name) || null;
if (nameOnly || session) return session;
}
if (!host) return null;
host = host.toLowerCase();
const hosts = sessions.filter(s => s.hostname.toLowerCase() === host);
if (!username) return hosts[0] || null;
username = username.toLowerCase();
return hosts.find(s => s.username.toLowerCase() === username) || null;
}
Loading…
Cancel
Save