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.
## 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
Add SSH FS configs to "sshfs.configs" in your User Settings:
```js
@ -20,17 +26,52 @@ Add SSH FS configs to "sshfs.configs" in your User Settings:
// Username to login with
"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
"agent": "pageant"
// Instead of using an agent, we can also just use a password
"password": "CorrectHorseBatteryStaple"
// Or a private key (raw key, OpenSSH format)
// (can also be a public key for host-based authentication)
"privateKey": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnN...",
// Should the private key be encrypted
"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)*
* ~~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 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
* Configuring a deleted (but active) configuration should show the old config
* Currently it'll open a new default configuration file for it

13
package-lock.json generated

@ -1,6 +1,6 @@
{
"name": "vscode-sshfs",
"version": "0.0.1",
"version": "1.0.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -29,6 +29,12 @@
"@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": {
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
@ -2423,6 +2429,11 @@
"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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",

@ -208,6 +208,10 @@
"passphrase": {
"type": "string",
"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": {
"@types/node": "^7.0.43",
"@types/ssh2": "^0.5.35",
"@types/winreg": "^1.2.30",
"typescript": "^2.5.2",
"vscode": "^1.1.17"
},
"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 * as vscode from 'vscode';
import { getSession as getPuttySession, PuttySession } from './putty';
import SSHFileSystem, { EMPTY_FILE_SYSTEM } from './sshFileSystem';
import { toPromise } from './toPromise';
@ -14,6 +17,7 @@ async function assertFs(man: Manager, uri: vscode.Uri) {
export interface FileSystemConfig extends ConnectConfig {
name: string;
root?: string;
putty?: string | boolean;
}
function createTreeItem(manager: Manager, name: string): vscode.TreeItem {
@ -126,11 +130,38 @@ export class Manager implements vscode.FileSystemProvider, vscode.TreeDataProvid
if (promise) return promise;
// config = config || this.memento.get(`fs.config.${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) {
throw new Error(`A SSH filesystem with the name '${name}' doesn't exist`);
}
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();
client.on('ready', () => {
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