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

Add OpenSSH tunneling capability #20

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
14 changes: 13 additions & 1 deletion js/languages/en_AU.json
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,16 @@
"placeholderPort": "The port to your server",
"torLabel": "Tor",
"useTor": "Use Tor",
"sshLabel": "SSH Tunnel",
"useSSH": "Use an SSH tunnel",
"sshHostLabel": "SSH Host",
"sshHostPlaceholder": "127.0.0.1:22",
"sshUsernameLabel": "SSH Username",
"sshUsernamePlaceholder": "root",
"sshPasswordLabel": "SSH Password",
"sshPasswordPlaceholder": "Password if using password-based authentication otherwise blank",
"sshKeyfileLabel": "SSH Private Key",
"sshKeyfilePlaceholder": "~/.ssh/id_rsa",
"configureTorMessage": "Tor has been detected. If you would like to use Tor, please check the \"Use Tor\" box below and specify your \"Tor SOCKS5 proxy\" setting. Click Save to confirm your decision.",
"torNotAvailableMessage": "Tor was not detected. If you would like to proceed without Tor, uncheck \"Use Tor\" below and click Save. Otherwise start up Tor and then retry connecting.",
"torProxyLabel": "Tor SOCKS5 proxy",
Expand Down Expand Up @@ -1585,7 +1595,9 @@
"invalidIp": "This does not appear to be a valid IP address.",
"providePortAsNumber": "Please provide a port as a number.",
"provideValidPortRange": "Please provide a number between 0 and 65535.",
"invalidTorProxy": "The value does not appear to be in the right format. It should be in the format ip-address:port, e.g. 127.0.0.1:9150. The port must be a number between 0 and 65535."
"invalidTorProxy": "The value does not appear to be in the right format. It should be in the format ip-address:port, e.g. 127.0.0.1:9150. The port must be a number between 0 and 65535.",
"invalidSSHPassword": "Please provide either a password or key file.",
"invalidSSHHost": "Please provide a valid IP address"
},
"shippingAddressModelErrors": {
"provideName": "Please provide a name.",
Expand Down
14 changes: 13 additions & 1 deletion js/languages/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,16 @@
"placeholderPort": "The port to your server",
"torLabel": "Tor",
"useTor": "Use Tor",
"sshLabel": "SSH Tunnel",
"useSSH": "Use an SSH tunnel",
"sshHostLabel": "SSH Host",
"sshHostPlaceholder": "127.0.0.1:22",
"sshUsernameLabel": "SSH Username",
"sshUsernamePlaceholder": "root",
"sshPasswordLabel": "SSH Password",
"sshPasswordPlaceholder": "Password if using password-based authentication otherwise blank",
"sshKeyfileLabel": "SSH Private Key",
"sshKeyfilePlaceholder": "~/.ssh/id_rsa",
"configureTorMessage": "Tor has been detected. If you would like to use Tor, please check the \"Use Tor\" box below and specify your \"Tor SOCKS5 proxy\" setting. Click Save to confirm your decision.",
"torNotAvailableMessage": "Tor was not detected. If you would like to proceed without Tor, uncheck \"Use Tor\" below and click Save. Otherwise start up Tor and then retry connecting.",
"torProxyLabel": "Tor SOCKS5 proxy",
Expand Down Expand Up @@ -1585,7 +1595,9 @@
"invalidIp": "This does not appear to be a valid IP address.",
"providePortAsNumber": "Please provide a port as a number.",
"provideValidPortRange": "Please provide a number between 0 and 65535.",
"invalidTorProxy": "The value does not appear to be in the right format. It should be in the format ip-address:port, e.g. 127.0.0.1:9150. The port must be a number between 0 and 65535."
"invalidTorProxy": "The value does not appear to be in the right format. It should be in the format ip-address:port, e.g. 127.0.0.1:9150. The port must be a number between 0 and 65535.",
"invalidSSHPassword": "Please provide either a password or key file.",
"invalidSSHHost": "Please provide a valid IP address"
},
"shippingAddressModelErrors": {
"provideName": "Please provide a name.",
Expand Down
32 changes: 30 additions & 2 deletions js/models/ServerConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ export default class extends BaseModel {
SSL: false,
default: false,
useTor: false,
useSSH: false,
sshHost: '127.0.0.1',
sshUsername: 'root',
sshPassword: '',
sshKeyfile: '~/.ssh/id_rsa',
confirmedTor: false,
torProxy: '127.0.0.1:9150',
dontShowTorExternalLinkWarning: false,
Expand Down Expand Up @@ -99,6 +104,29 @@ export default class extends BaseModel {
}
}

if (attrs.useSSH) {
let valid = true;
const split = attrs.torProxy.split(':');

if (split.length !== 2) {
valid = false;
} else {
if (!is.ip(split[0])) {
valid = false;
} else if (!is.within(parseInt(split[1], 10), -1, 65536)) {
valid = false;
}
}

if (!valid) {
addError('sshTunnel', app.polyglot.t('serverConfigModelErrors.invalidSSHHost'));
}

if (!attrs.sshPassword && !attrs.sshKeyfile) {
addError('sshTunnel', app.polyglot.t('serverConfigModelErrors.invalidSSHPassword'));
}
}

if (!attrs.default) {
if (attrs.port === undefined || attrs.port === '') {
addError('port', app.polyglot.t('serverConfigModelErrors.provideValue'));
Expand Down Expand Up @@ -155,8 +183,8 @@ export default class extends BaseModel {
* your machine. It may be the local bundled server or it may be a locally
* run stand-alone server.
*/
isLocalServer(ip = this.get('serverIp')) {
return ip === 'localhost' || ip === '127.0.0.1';
isLocalServer(ip = this.get('serverIp'), useSSH = this.get('useSSH')) {
return (ip === 'localhost' || ip === '127.0.0.1') && !useSSH;
}

isTorPwRequired() {
Expand Down
52 changes: 52 additions & 0 deletions js/templates/modals/connectionManagement/configurationForm.html
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ <h2 class="h3 clrT txUnl txCtr"><%= ob.title %></h2>
<label for="serverConfigUseTor"><%= ob.polyT('connectionManagement.configurationForm.useTor') %></label>
</div>
</div>
<div class="flexRow">
<div class="col3">
<label><%= ob.polyT('connectionManagement.configurationForm.sshLabel') %></label>
</div>
<div class="col9">
<% if (ob.errors.useSSH) print(ob.formErrorTmpl({ errors: ob.errors.useSSH })) %>
<input type="checkbox" id="serverConfigUseSSH" name="useSSH" <% if (ob.useSSH) print('checked') %> />
<label for="serverConfigUseSSH"><%= ob.polyT('connectionManagement.configurationForm.useSSH') %></label>
</div>
</div>
<div class="js-torDetails torDetails padMdKids padStack <% if (!ob.useTor) print('hide') %>">
<% if (!ob.default) { %>
<div>
Expand Down Expand Up @@ -135,6 +145,48 @@ <h2 class="h3 clrT txUnl txCtr"><%= ob.title %></h2>
</div>
</div>
</div>
<div class="js-sshDetails torDetails padMdKids padStack <% if (!ob.useSSH) print('hide') %>">
<div class="flexRow">
<div class="col3">
<label class="required"><%= ob.polyT('connectionManagement.configurationForm.sshHostLabel') %></label>
</div>
<div class="col9">
<input type="text" class="clrBr clrSh2 required" name="sshHost" id="serverConfigHost" value="<%= ob.sshHost %>" placeholder="<%= ob.polyT('connectionManagement.configurationForm.sshHostPlaceholder') %>">
</div>
</div>
<div class="flexRow">
<div class="col3">
<label class="required"><%= ob.polyT('connectionManagement.configurationForm.sshUsernameLabel') %></label>
</div>
<div class="col9">
<input type="text" class="clrBr clrSh2 required" name="sshUsername" id="sshConfigUsername" value="<%= ob.sshUsername %>" placeholder="<%= ob.polyT('connectionManagement.configurationForm.sshUsernamePlaceholder') %>">
</div>
</div>
<div class="flexRow">
<div class="col3">
<label><%= ob.polyT('connectionManagement.configurationForm.sshPasswordLabel') %></label>
</div>
<div class="col9">
<input type="password" class="clrBr clrSh2" name="sshPassword" id="serverConfigPassword" value="<%= ob.sshPassword %>" placeholder="<%= ob.polyT('connectionManagement.configurationForm.sshPasswordPlaceholder') %>">
</div>
</div>
<div class="flexRow">
<div class="col3">
<label><%= ob.polyT('connectionManagement.configurationForm.sshKeyfileLabel') %></label>
</div>
<div class="col9">
<div class="flexRow">
<div class="col9">
<input type="text" class="clrBr clrSh2" name="sshKeyFile" id="serverConfigKeyfile" value="<%= ob.sshKeyfile %>"/>
</div>
<div class="col3">
<input type="file" class="choose-file" name="sshKeyFileSelector" value="<%= ob.sshKeyfile %>" id="serverConfigKeyfileChooser" hidden>
<a class="btn clrP clrBr clrSh2 center-button-with-input js-select-key">Select key</a>
</div>
</div>
</div>
</div>
</div>
</form>

<hr class="clrBr" />
Expand Down
79 changes: 60 additions & 19 deletions js/utils/serverConnect.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { remote, ipcRenderer } from 'electron';
import _ from 'underscore';
import $ from 'jquery';
import { Events } from 'backbone';
import sshTunnel from 'open-ssh-tunnel';
import fs from 'fs';
import Socket from '../utils/Socket';
import { guid } from './';
import app from '../app';
Expand Down Expand Up @@ -33,6 +35,7 @@ export { events };
const getLocalServer = _.once(() => (remote.getGlobal('localServer')));

let currentConnection = null;
let currentSSHServer = null;
let debugLog = '';

function log(msg) {
Expand Down Expand Up @@ -142,6 +145,10 @@ function authenticate(server) {
* If we're currently connected to a server, this method will disconnect the connection.
*/
export function disconnect() {
if (currentSSHServer !== null) {
currentSSHServer.close();
currentSSHServer = null;
}
const curCon = getCurrentConnection();
if (curCon && curCon.socket) curCon.socket.close();
}
Expand Down Expand Up @@ -401,25 +408,59 @@ export default function connect(server, options = {}) {
});
}
} else {
socketConnectAttempt = socketConnect(socket)
.done(() => {
if (server.needsAuthentication()) {
innerConnectDeferred.notify('authenticating');
authenticate(server)
.done(() => innerConnectDeferred.resolve('connected'))
.fail((reason, e) => {
innerConnectDeferred.reject('authentication-failed', { failedAuthEvent: e });
});
} else {
innerConnectDeferred.resolve('connected');
}
}).fail((reason, e) => {
if (reason === 'canceled') {
innerConnectDeferred.reject('canceled');
} else {
innerConnectDeferred.reject('socket-connect-failed', { socketCloseEvent: e });
}
});
const afterSSHInitialized = (sshServer) => {
currentSSHServer = sshServer;
socketConnect(socket)
.done(() => {
if (server.needsAuthentication()) {
innerConnectDeferred.notify('authenticating');
authenticate(server)
.done(() => innerConnectDeferred.resolve('connected'))
.fail((reason, e) => {
sshServer.close();
innerConnectDeferred.reject('authentication-failed', { failedAuthEvent: e });
});
} else {
innerConnectDeferred.resolve('connected');
}
}).fail((reason, e) => {
if (reason === 'canceled') {
sshServer.close();
innerConnectDeferred.reject('canceled');
} else {
sshServer.close();
innerConnectDeferred.reject('socket-connect-failed', { socketCloseEvent: e });
}
});
};
if (server.get('useSSH')) {
const keyExists = fs.existsSync(server.get('sshKeyfile'));
if (!keyExists && !server.get('sshPassword')) {
innerConnectDeferred.reject('invalid-ssh-key-file');
return;
}
let privateKey = undefined;
if (keyExists) {
privateKey = fs.readFileSync(server.get('sshKeyfile'));
}
sshTunnel({
host: server.get('sshHost'),
username: server.get('sshUsername'),
password: server.get('sshPassword'),
srcPort: 5002,
srcAddr: '127.0.0.1',
dstPort: 5002,
dstAddr: '127.0.0.1',
readyTimeout: 5000,
forwardTimeout: 5000,
localPort: 5002,
localAddr: '127.0.0.1',
privateKey,
}).then(afterSSHInitialized)
.catch((e) => innerConnectDeferred.reject('ssh-connection-failed', e));
} else {
afterSSHInitialized({ close: () => 0 }); // harmless hack to fake an SSH server interface
}
}
};

Expand Down
16 changes: 16 additions & 0 deletions js/views/modals/connectionManagement/ConfigurationForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ export default class extends baseVw {
'click .js-saveConfirmCancel': 'onClickSaveConfirmCancel',
'change #serverConfigServerIp': 'onChangeServerIp',
'change [name=useTor]': 'onChangeUseTor',
'change [name=useSSH]': 'onChangeUseSSH',
'click .js-select-key': 'onClickSelectKey',
'change [name=sshKeyFileSelector]': 'onChangeSSHKey',
};
}

Expand Down Expand Up @@ -133,6 +136,19 @@ export default class extends baseVw {
.toggleClass('hide', !e.target.checked);
}

onChangeUseSSH(e) {
this.getCachedEl('.js-sshDetails')
.toggleClass('hide', !e.target.checked);
}

onClickSelectKey() {
this.getCachedEl('#serverConfigKeyfileChooser').click();
}

onChangeSSHKey(e) {
this.getCachedEl('#serverConfigKeyfile')[0].value = e.target.files[0].path;
}

save() {
const formData = this.getFormData(this.$formFields);
this.model.set({
Expand Down
Loading