').appendTo(outer);
- // entry.body = body;
- // if (entry.path) {
- // entry.removeButton = $('
')
- // // .hide()
- // .appendTo(header)
- // .click(function(e) {
- // entry.removed = true;
- // body.fadeOut(100);
- // entry.header.css("text-decoration","line-through")
- // entry.header.css("font-style","italic")
- // if (entry.copyToClipboard) {
- // entry.copyToClipboard.hide();
- // }
- // $(this).hide();
- // });
- // if (entry.data) {
- // entry.copyToClipboard = $('
')
- // // .hide()
- // .appendTo(header)
- // .click(function(e) {
- // var textarea = document.createElement("textarea");
- // textarea.style.position = 'fixed';
- // textarea.style.top = 0;
- // textarea.style.left = 0;
- // textarea.style.width = '2em';
- // textarea.style.height = '2em';
- // textarea.style.padding = 0;
- // textarea.style.border = 'none';
- // textarea.style.outline = 'none';
- // textarea.style.boxShadow = 'none';
- // textarea.style.background = 'transparent';
- // textarea.value = entry.data;
- // document.body.appendChild(textarea);
- // textarea.select();
- // try {
- // var ret = document.execCommand('copy');
- // var msg = ret ? 'successful' : 'unsuccessful';
- // console.log('Copy text command was ' + msg);
- // } catch (err) {
- // console.log('Oops unable to copy');
- // }
- // document.body.removeChild(textarea);
- // });
- // }
- // }
- // }
- // });
+ var sshkeyInputRow = $('
').hide().appendTo(gitconfigContainer);
+ var sshkeyNameLabel = $('
').text('Key Name').appendTo(sshkeyInputRow);
+ var sshkeyNameInput = $('
').appendTo(sshkeyInputRow);
+ var sshkeyPassphraseLabel = $('
').text('Passphrase').appendTo(sshkeyInputRow);
+ var sshkeyPassphraseInput = $('
').appendTo(sshkeyInputRow);
+ var sshkeySamePassphraseLabel = $('
').text('Same Passphrase').appendTo(sshkeyInputRow);
+ var sshkeySamePassphraseInput = $('
').appendTo(sshkeyInputRow);
- // var remoteListAddButton = row.find(".red-ui-editableList-addButton").hide();
+ var formButtonArea = $('
').appendTo(gitconfigContainer);
+ var formButtons = $('
')
+ .hide().appendTo(formButtonArea);
+
+ function hideSSHKeyGenerateForm() {
+ editSshKeyListButton.show();
+ formButtons.hide();
+ sshkeyInputRow.hide();
+ sshkeyNameInput.val("");
+ sshkeyPassphraseInput.val("");
+ sshkeySamePassphraseInput.val("");
+ if ( sshkeyNameInput.hasClass('input-error') ) {
+ sshkeyNameInput.removeClass('input-error');
+ }
+ if ( sshkeyPassphraseInput.hasClass('input-error') ) {
+ sshkeyPassphraseInput.removeClass('input-error');
+ }
+ if ( sshkeySamePassphraseInput.hasClass('input-error') ) {
+ sshkeySamePassphraseInput.removeClass('input-error');
+ }
+ $(".projects-dialog-sshkey-list-button-remove").hide();
+ }
+
+ $('
')
+ .appendTo(formButtons)
+ .click(function(evt) {
+ evt.preventDefault();
+ hideSSHKeyGenerateForm();
+ });
+ var generateButton = $('
')
+ .appendTo(formButtons)
+ .click(function(evt) {
+ evt.preventDefault();
+ if ( sshkeyNameInput.hasClass('input-error') ) {
+ sshkeyNameInput.removeClass('input-error');
+ }
+ if ( sshkeyPassphraseInput.hasClass('input-error') ) {
+ sshkeyPassphraseInput.removeClass('input-error');
+ }
+ if ( sshkeySamePassphraseInput.hasClass('input-error') ) {
+ sshkeySamePassphraseInput.removeClass('input-error');
+ }
+ var valid = true;
+ if ( sshkeyNameInput.val() === "" ) {
+ sshkeyNameInput.addClass('input-error');
+ valid = false;
+ }
+ if ( sshkeyPassphraseInput.val() !== sshkeySamePassphraseInput.val() ) {
+ sshkeySamePassphraseInput.addClass('input-error');
+ valid = false;
+ }
+ if ( valid ) {
+ sendSSHKeyManagementAPI("GENERATE_KEY",
+ {
+ name: sshkeyNameInput.val(),
+ email: gitEmailInput.val(),
+ password: sshkeyPassphraseInput.val(),
+ size: 4096
+ },
+ gitconfigContainer,
+ function() {
+ hideSSHKeyGenerateForm();
+ utils.refreshSSHKeyList(sshkeysList);
+ },
+ function(err) {
+ console.log('err message:', err.message);
+ if ( err.message.includes('Some SSH Keyfile exists') ) {
+ sshkeyNameInput.addClass('input-error');
+ }
+ else if ( err.message.includes('Failed to generate ssh key files') ) {
+ sshkeyPassphraseInput.addClass('input-error');
+ sshkeySamePassphraseInput.addClass('input-error');
+ }
+ }
+ );
+ }
+ });
+ }
+
+ function sendSSHKeyManagementAPI(type, param, overlay, successCallback, failCallback) {
+ var url;
+ var method;
+ var payload;
+ switch(type) {
+ case 'GET_KEY_LIST':
+ method = 'GET';
+ url = "settings/user/keys";
+ break;
+ case 'GET_KEY_DETAIL':
+ method = 'GET';
+ url = "settings/user/keys/" + param;
+ break;
+ case 'GENERATE_KEY':
+ method = 'POST';
+ url = "settings/user/keys";
+ payload= param;
+ break;
+ case 'DELETE_KEY':
+ method = 'DELETE';
+ url = "settings/user/keys/" + param;
+ break;
+ default:
+ console.error('Unexpected type....');
+ return;
+ }
+ // var spinner = utils.addSpinnerOverlay(gitconfigContainer);
+ var spinner = overlay ? utils.addSpinnerOverlay(overlay) : null;
+
+ var done = function(err) {
+ if ( spinner ) {
+ spinner.remove();
+ }
+ if (err) {
+ console.log(err);
+ return;
+ }
+ };
+
+ console.log('method:', method);
+ console.log('url:', url);
+
+ utils.sendRequest({
+ url: url,
+ type: method,
+ responses: {
+ 0: function(error) {
+ if ( failCallback ) {
+ failCallback(error);
+ }
+ done(error);
+ },
+ 200: function(data) {
+ if ( successCallback ) {
+ successCallback(data);
+ }
+ done();
+ },
+ 400: {
+ 'unexpected_error': function(error) {
+ console.log(error);
+ if ( failCallback ) {
+ failCallback(error);
+ }
+ done(error);
+ }
+ },
+ }
+ },payload);
+ }
+
+ var dialog;
+ var dialogBody;
+ function createPublicKeyDialog() {
+ dialog = $('
')
+ .appendTo("body")
+ .dialog({
+ modal: true,
+ autoOpen: false,
+ width: 600,
+ resize: false,
+ open: function(e) {
+ $(this).parent().find(".ui-dialog-titlebar-close").hide();
+ },
+ close: function(e) {
+
+ }
+ });
+ dialogBody = dialog.find("form");
+ dialog.dialog('option', 'title', 'SSH public key');
+ dialog.dialog('option', 'buttons', [
+ {
+ text: RED._("common.label.close"),
+ click: function() {
+ $( this ).dialog( "close" );
+ }
+ },
+ {
+ text: "Copy to Clipboard",
+ click: function() {
+ var target = document.getElementById('public-key-data');
+ document.getSelection().selectAllChildren(target);
+ var ret = document.execCommand('copy');
+ var msg = ret ? 'successful' : 'unsuccessful';
+ console.log('Copy text command was ' + msg);
+ $( this ).dialog("close");
+ }
+ }
+ ]);
+ dialog.dialog({position: { 'my': 'center', 'at': 'center', 'of': window }});
+ var container = $('
');
+ $('
').appendTo(container);
+ $('
').appendTo(container);
+ dialogBody.append(container);
+ }
+
+ function setDialogContext(name, data) {
+ var title = dialog.find("div.projects-dialog-ssh-public-key-name");
+ title.text(name);
+ var context = dialog.find("div.projects-dialog-ssh-public-key>pre");
+ context.text(data);
}
function createSettingsPane(activeProject) {
var pane = $('
');
createRemoteRepositorySection(pane);
+ createPublicKeyDialog();
return pane;
}
diff --git a/editor/js/ui/projects.js b/editor/js/ui/projects.js
index 894dae4e0..a5e59be56 100644
--- a/editor/js/ui/projects.js
+++ b/editor/js/ui/projects.js
@@ -465,6 +465,7 @@ RED.projects = (function() {
var projectRepoUserInput;
var projectRepoPasswordInput;
var projectNameSublabel;
+ var projectRepoSSHKeySelect;
var projectRepoPassphrase;
var projectRepoRemoteName
var projectRepoBranch;
@@ -537,12 +538,18 @@ RED.projects = (function() {
if (/^(?:ssh|[\d\w\.\-_]+@[\w\.]+):(?:\/\/)?/.test(repo)) {
$(".projects-dialog-screen-create-row-creds").hide();
$(".projects-dialog-screen-create-row-passphrase").show();
+ $(".projects-dialog-screen-create-row-sshkey").show();
+ if ( !getSelectedSSHKey(projectRepoSSHKeySelect) ) {
+ valid = false;
+ }
} else if (/^https?:\/\//.test(repo)) {
$(".projects-dialog-screen-create-row-creds").show();
$(".projects-dialog-screen-create-row-passphrase").hide();
+ $(".projects-dialog-screen-create-row-sshkey").hide();
} else {
$(".projects-dialog-screen-create-row-creds").show();
$(".projects-dialog-screen-create-row-passphrase").hide();
+ $(".projects-dialog-screen-create-row-sshkey").hide();
}
@@ -569,7 +576,7 @@ RED.projects = (function() {
}
}
}
-
+
$("#projects-dialog-create").prop('disabled',!valid).toggleClass('disabled ui-button-disabled ui-state-disabled',!valid);
}
@@ -749,6 +756,16 @@ RED.projects = (function() {
$('
').appendTo(subrow);
projectRepoPasswordInput = $('
').appendTo(subrow);
+ row = $('
').hide().appendTo(container);
+ $('
').appendTo(row);
+ projectRepoSSHKeySelect = createSSHKeyList({
+ height: "120px",
+ selectAction: function(entry, header) {
+ $('.projects-dialog-sshkey-list-entry').removeClass('selected');
+ header.addClass('selected');
+ }
+ }).appendTo(row);
+
row = $('
').hide().appendTo(container);
$('
').appendTo(row);
projectRepoPassphrase = $('
').appendTo(row);
@@ -818,14 +835,36 @@ RED.projects = (function() {
projectData.copy = copyProject.name;
} else if (projectType === 'clone') {
// projectData.credentialSecret = projectSecretInput.val();
- projectData.git = {
- remotes: {
- 'origin': {
- url: projectRepoInput.val(),
- username: projectRepoUserInput.val(),
- password: projectRepoPasswordInput.val()
- }
+ var repoUrl = projectRepoInput.val();
+ var metaData = {};
+ if (/^(?:ssh|[\d\w\.\-_]+@[\w\.]+):(?:\/\/)?/.test(repoUrl)) {
+ var selected = getSelectedSSHKey(projectRepoSSHKeySelect);
+ if ( selected && selected.name ) {
+ projectData.git = {
+ remotes: {
+ 'origin': {
+ url: repoUrl,
+ key_file: selected.name,
+ passphrase: projectRepoPassphrase.val()
+ }
+ }
+ };
}
+ else {
+ console.log("Error! Can't get selected SSH key path.");
+ return;
+ }
+ }
+ else {
+ projectData.git = {
+ remotes: {
+ 'origin': {
+ url: repoUrl,
+ username: projectRepoUserInput.val(),
+ password: projectRepoPasswordInput.val()
+ }
+ }
+ };
}
}
@@ -855,6 +894,8 @@ RED.projects = (function() {
projectRepoUserInput.addClass("input-error");
projectRepoPasswordInput.addClass("input-error");
// getRepoAuthDetails(req);
+ projectRepoSSHKeySelect.addClass("input-error");
+ projectRepoPassphrase.addClass("input-error");
console.log("git auth error",error);
},
'unexpected_error': function(error) {
@@ -1115,6 +1156,87 @@ RED.projects = (function() {
return container;
}
+ function createSSHKeyList(options) {
+ options = options || {};
+ var minHeight = "33px";
+ var maxHeight = options.height || "120px";
+ var container = $('
',{style:"max-height: "+maxHeight+";"});
+
+ var sshkeyList = $('
',{class:"projects-dialog-sshkey-list", style:"max-height:"+maxHeight+";min-height:"+minHeight+";"}).appendTo(container).editableList({
+ addButton: false,
+ scrollOnAdd: false,
+ addItem: function(row,index,entry) {
+ var header = $('',{class:"projects-dialog-sshkey-list-entry"}).appendTo(row);
+ $('').appendTo(header);
+ $('').text(entry.name).appendTo(header);
+ var deleteButton = $('',{class:"projects-dialog-sshkey-list-entry-icon projects-dialog-sshkey-list-button-remove editor-button editor-button-small"})
+ .hide()
+ .appendTo(header)
+ .click(function(evt) {
+ evt.preventDefault();
+ console.log('deleteButton --- click');
+ if ( options.deleteAction ) {
+ options.deleteAction(entry, header);
+ }
+ return false;
+ });
+ $('',{class:"fa fa-trash-o"}).appendTo(deleteButton);
+ header.addClass("selectable");
+ row.click(function(evt) {
+ if ( !deleteButton.is(":visible") ) {
+ if ( options.selectAction ) {
+ options.selectAction(entry, header);
+ }
+ $.data(container[0], 'selected', entry);
+ }
+ return false;
+ })
+ }
+ });
+ $.getJSON("settings/user/keys", function(data) {
+ data.keys.forEach(function(key) {
+ if ( sshkeyList ) {
+ sshkeyList.editableList('addItem',key);
+ }
+ else {
+ console.log('[create] Error! selectedSSHKey is not set up.');
+ }
+ });
+ });
+ if ( sshkeyList ) {
+ sshkeyList.addClass("projects-dialog-sshkey-list-small");
+ $.data(container[0], 'selected', null);
+ $.data(container[0], 'sshkeys', sshkeyList);
+ }
+ return container;
+ }
+ function getSelectedSSHKey(container) {
+ var selected = $.data(container[0], 'selected');
+ if ( container && selected ) {
+ return selected;
+ }
+ else {
+ return null;
+ }
+ }
+ function refreshSSHKeyList(container) {
+ var sshkeyList = $.data(container[0], 'sshkeys');
+ if ( container && sshkeyList ) {
+ sshkeyList.empty();
+ $.getJSON("settings/user/keys", function(data) {
+ var keyList = $.data(container[0], 'sshkeys');
+ data.keys.forEach(function(key) {
+ if ( keyList ) {
+ keyList.editableList('addItem',key);
+ }
+ else {
+ console.log('[refresh] Error! selectedSSHKey is not set up.');
+ }
+ });
+ });
+ }
+ }
+
function sendRequest(options,body) {
// dialogBody.hide();
console.log(options.url,body);
@@ -1431,7 +1553,9 @@ RED.projects = (function() {
var projectsAPI = {
sendRequest:sendRequest,
createBranchList:createBranchList,
- addSpinnerOverlay:addSpinnerOverlay
+ addSpinnerOverlay:addSpinnerOverlay,
+ createSSHKeyList:createSSHKeyList,
+ refreshSSHKeyList:refreshSSHKeyList
};
RED.projects.settings.init(projectsAPI);
RED.projects.userSettings.init(projectsAPI);
diff --git a/editor/sass/projects.scss b/editor/sass/projects.scss
index fa21b5144..a97bcb98e 100644
--- a/editor/sass/projects.scss
+++ b/editor/sass/projects.scss
@@ -675,6 +675,92 @@
}
}
+.projects-dialog-sshkey-list {
+ li {
+ padding: 0 !important;
+ }
+ &.projects-dialog-sshkey-list-small {
+ .projects-dialog-sshkey-list-entry {
+ padding: 6px 0;
+ i {
+ font-size: 1em;
+ }
+ }
+ .projects-dialog-sshkey-list-entry-name {
+ font-size: 1em;
+ }
+ .projects-dialog-sshkey-list-entry-current {
+ margin-right: 10px;
+ padding-top: 2px;
+ }
+ }
+}
+.red-ui-editableList-container {
+ .projects-dialog-sshkey-list {
+ li:last-child {
+ border-bottom: 0px none;
+ }
+ }
+}
+.projects-dialog-sshkey-list-entry {
+ padding: 12px 0;
+ border-left: 3px solid #fff;
+ border-right: 3px solid #fff;
+ &.sshkey-list-entry-current {
+ &:not(.selectable) {
+ background: #f9f9f9;
+ }
+ i {
+ color: #999;
+ }
+ }
+ &.selectable {
+ cursor: pointer;
+ &:hover {
+ background: #f3f3f3;
+ border-left-color: #aaa;
+ border-right-color: #aaa;
+ }
+ }
+ i {
+ color: #ccc;
+ font-size: 2em;
+
+ }
+ &.selected {
+ background: #efefef;
+ border-left-color:#999;
+ border-right-color:#999;
+ }
+ span {
+ display: inline-block;
+ vertical-align:middle;
+ }
+ .projects-dialog-sshkey-list-entry-icon {
+ margin: 0 10px 0 5px;
+ }
+ .projects-dialog-sshkey-list-entry-name {
+ font-size: 1.2em;
+ }
+ .projects-dialog-sshkey-list-entry-current {
+ float: right;
+ margin-right: 20px;
+ font-size: 0.9em;
+ color: #999;
+ padding-top: 4px;
+ }
+ .projects-dialog-sshkey-list-button-remove {
+ position: absolute;
+ right: 4px;
+ }
+}
+
+div.projects-dialog-ssh-public-key {
+ pre {
+ word-break: break-all;
+ }
+}
+
.projects-dialog-list {
position: relative;
.red-ui-editableList-container {
diff --git a/package.json b/package.json
index 044d485de..3d9dbabcd 100644
--- a/package.json
+++ b/package.json
@@ -59,6 +59,7 @@
"raw-body":"2.2.0",
"semver": "5.3.0",
"sentiment":"2.1.0",
+ "ssh-keygen":"0.4.1",
"uglify-js":"3.0.20",
"when": "3.7.8",
"ws": "1.1.1",
diff --git a/red/api/editor/index.js b/red/api/editor/index.js
index b9f4d539a..21cc29b9e 100644
--- a/red/api/editor/index.js
+++ b/red/api/editor/index.js
@@ -97,6 +97,10 @@ module.exports = {
// User Settings
editorApp.post("/settings/user",needsPermission("settings.write"),info.updateUserSettings,apiUtil.errorHandler);
+ // SSH keys
+ var sshkeys = require("./sshkeys");
+ sshkeys.init(runtime);
+ editorApp.use("/settings/user/keys",sshkeys.app());
return editorApp;
}
diff --git a/red/api/editor/sshkeys.js b/red/api/editor/sshkeys.js
new file mode 100644
index 000000000..08ae241ed
--- /dev/null
+++ b/red/api/editor/sshkeys.js
@@ -0,0 +1,125 @@
+/**
+ * Copyright JS Foundation and other contributors, http://js.foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ **/
+
+var express = require("express");
+var os = require("os");
+var runtime;
+var settings;
+var needsPermission = require("../auth").needsPermission;
+
+function getUsername(userObj) {
+ var username = '.default';
+ if ( userObj && userObj.name ) {
+ username = userObj.name;
+ }
+ return username;
+}
+
+module.exports = {
+ init: function(_runtime) {
+ runtime = _runtime;
+ settings = runtime.settings;
+ },
+ app: function() {
+ var app = express();
+
+ // SSH keys
+
+ // List all SSH keys
+ app.get("/", needsPermission("settings.read"), function(req,res) {
+ var username = getUsername(req.user);
+ runtime.storage.sshkeys.listSSHKeys(username)
+ .then(function(list) {
+ res.json({
+ keys: list
+ });
+ })
+ .catch(function(err) {
+ // console.log(err.stack);
+ if (err.code) {
+ res.status(400).json({error:err.code, message: err.message});
+ } else {
+ res.status(400).json({error:"unexpected_error", message:err.toString()});
+ }
+ });
+ });
+
+ // Get SSH key detail
+ app.get("/:id", needsPermission("settings.read"), function(req,res) {
+ var username = getUsername(req.user);
+ // console.log('username:', username);
+ runtime.storage.sshkeys.getSSHKey(username, req.params.id)
+ .then(function(data) {
+ res.json({
+ publickey: data
+ });
+ })
+ .catch(function(err) {
+ console.log(err.stack);
+ if (err.code) {
+ res.status(400).json({error:err.code, message: err.message});
+ } else {
+ res.status(400).json({error:"unexpected_error", message:err.toString()});
+ }
+ });
+ });
+
+ // Generate a SSH key
+ app.post("/", needsPermission("settings.write"), function(req,res) {
+ var username = getUsername(req.user);
+ // console.log('req.body:', req.body);
+ if ( req.body && req.body.name ) {
+ runtime.storage.sshkeys.generateSSHKey(username, req.body)
+ .then(function(name) {
+ // console.log('generate key --- success name:', name);
+ res.json({
+ name: name
+ });
+ })
+ .catch(function(err) {
+ console.log(err.stack);
+ if (err.code) {
+ res.status(400).json({error:err.code, message: err.message});
+ } else {
+ res.status(400).json({error:"unexpected_error", message:err.toString()});
+ }
+ });
+ }
+ else {
+ res.status(400).json({error:"unexpected_error", message:"You need to have body or body.name"});
+ }
+ });
+
+ // Delete a SSH key
+ app.delete("/:id", needsPermission("settings.write"), function(req,res) {
+ var username = getUsername(req.user);
+ runtime.storage.sshkeys.deleteSSHKey(username, req.params.id)
+ .then(function(ret) {
+ res.status(204).end();
+ })
+ .catch(function(err) {
+ console.log(err.stack);
+ if (err.code) {
+ res.status(400).json({error:err.code, message: err.message});
+ } else {
+ res.status(400).json({error:"unexpected_error", message:err.toString()});
+ }
+ });
+ });
+
+ return app;
+ }
+}
diff --git a/red/runtime/storage/index.js b/red/runtime/storage/index.js
index 9373b4b21..fe03da254 100644
--- a/red/runtime/storage/index.js
+++ b/red/runtime/storage/index.js
@@ -63,6 +63,9 @@ var storageModuleInterface = {
storageModuleInterface.projects = storageModule.projects;
}
}
+ if (storageModule.sshkeys) {
+ storageModuleInterface.sshkeys = storageModule.sshkeys;
+ }
return storageModule.init(runtime.settings,runtime);
},
getFlows: function() {
diff --git a/red/runtime/storage/localfilesystem/index.js b/red/runtime/storage/localfilesystem/index.js
index 2a57c7323..bcd8969dd 100644
--- a/red/runtime/storage/localfilesystem/index.js
+++ b/red/runtime/storage/localfilesystem/index.js
@@ -24,6 +24,7 @@ var library = require("./library");
var sessions = require("./sessions");
var runtimeSettings = require("./settings");
var projects = require("./projects");
+var sshkeys = require("./sshkeys");
var initialFlowLoadComplete = false;
var settings;
@@ -60,6 +61,7 @@ var localfilesystem = {
runtimeSettings.init(settings);
promises.push(library.init(settings));
promises.push(projects.init(settings, runtime));
+ promises.push(sshkeys.init(settings, runtime));
var packageFile = fspath.join(settings.userDir,"package.json");
var packagePromise = when.resolve();
@@ -94,7 +96,8 @@ var localfilesystem = {
saveSessions: sessions.saveSessions,
getLibraryEntry: library.getLibraryEntry,
saveLibraryEntry: library.saveLibraryEntry,
- projects: projects
+ projects: projects,
+ sshkeys: sshkeys
};
module.exports = localfilesystem;
diff --git a/red/runtime/storage/localfilesystem/projects/Project.js b/red/runtime/storage/localfilesystem/projects/Project.js
index e4d8aa752..579fc02f9 100644
--- a/red/runtime/storage/localfilesystem/projects/Project.js
+++ b/red/runtime/storage/localfilesystem/projects/Project.js
@@ -18,6 +18,7 @@
var fs = require('fs-extra');
var when = require('when');
var fspath = require("path");
+var os = require('os');
var gitTools = require("./git");
var util = require("../util");
@@ -753,6 +754,7 @@ function createProject(user, metadata) {
if (metadata.git && metadata.git.remotes && metadata.git.remotes.origin) {
var originRemote = metadata.git.remotes.origin;
var auth;
+ console.log('originRemote:', originRemote);
if (originRemote.hasOwnProperty("username") && originRemote.hasOwnProperty("password")) {
authCache.set(project,originRemote.url,username,{ // TODO: hardcoded remote name
username: originRemote.username,
@@ -761,6 +763,15 @@ function createProject(user, metadata) {
);
auth = authCache.get(project,originRemote.url,username);
}
+ else if (originRemote.hasOwnProperty("key_file") && originRemote.hasOwnProperty("passphrase")) {
+ var key_file_name = (username === '_') ? '.default' + '_' + originRemote.key_file : username + '_' + originRemote.key_file;
+ authCache.set(project,originRemote.url,username,{ // TODO: hardcoded remote name
+ key_path: fspath.join(projectsDir, ".sshkeys", key_file_name),
+ passphrase: originRemote.passphrase
+ }
+ );
+ auth = authCache.get(project,originRemote.url,username);
+ }
return gitTools.clone(originRemote,auth,projectPath).then(function(result) {
// Check this is a valid project
// If it is empty
diff --git a/red/runtime/storage/localfilesystem/projects/git/authServer.js b/red/runtime/storage/localfilesystem/projects/git/authServer.js
index a8048721a..a4ab209e8 100644
--- a/red/runtime/storage/localfilesystem/projects/git/authServer.js
+++ b/red/runtime/storage/localfilesystem/projects/git/authServer.js
@@ -45,7 +45,7 @@ var ResponseServer = function(auth) {
parts.push(data.substring(0, m));
data = data.substring(m);
var line = parts.join("");
- console.log("LINE",line);
+ console.log("LINE:",line);
parts = [];
if (line==='Username') {
connection.end(auth.username);
@@ -79,8 +79,54 @@ var ResponseServer = function(auth) {
});
}
+var ResponseSSHServer = function(auth) {
+ return new Promise(function(resolve, reject) {
+ server = net.createServer(function(connection) {
+ connection.setEncoding('utf8');
+ var parts = [];
+ connection.on('data', function(data) {
+ var m = data.indexOf("\n");
+ if (m !== -1) {
+ parts.push(data.substring(0, m));
+ data = data.substring(m);
+ var line = parts.join("");
+ console.log("LINE:",line);
+ parts = [];
+ if (line==='The') {
+ connection.end('yes');
+ // server.close();
+ } else if (line === 'Enter') {
+ connection.end(auth.passphrase);
+ // server.close();
+ } else {
+ }
+ }
+ if (data.length > 0) {
+ parts.push(data);
+ }
+
+ });
+ });
+
+ var listenPath = getListenPath();
+
+ server.listen(listenPath, function(ready) {
+ resolve({path:listenPath,close:function() { server.close(); }});
+ });
+ server.on('close', function() {
+ // console.log("Closing response server");
+ fs.removeSync(listenPath);
+ });
+ server.on('error',function(err) {
+ console.log("ResponseServer unexpectedError:",err.toString());
+ server.close();
+ reject(err);
+ })
+ });
+}
module.exports = {
- ResponseServer: ResponseServer
+ ResponseServer: ResponseServer,
+ ResponseSSHServer: ResponseSSHServer
}
diff --git a/red/runtime/storage/localfilesystem/projects/git/index.js b/red/runtime/storage/localfilesystem/projects/git/index.js
index a7a8d87b7..8bce2ce64 100644
--- a/red/runtime/storage/localfilesystem/projects/git/index.js
+++ b/red/runtime/storage/localfilesystem/projects/git/index.js
@@ -18,6 +18,7 @@ var when = require('when');
var exec = require('child_process').exec;
var spawn = require('child_process').spawn;
var authResponseServer = require('./authServer').ResponseServer;
+var sshResponseServer = require('./authServer').ResponseSSHServer;
var clone = require('clone');
var path = require("path");
@@ -50,6 +51,8 @@ function runGitCommand(args,cwd,env) {
err.code = "git_auth_failed";
} else if(/HTTP Basic: Access denied/.test(stderr)) {
err.code = "git_auth_failed";
+ } else if(/Permission denied \(publickey\)/.test(stderr)) {
+ err.code = "git_auth_failed";
} else if(/Connection refused/.test(stderr)) {
err.code = "git_connection_failed";
} else if (/commit your changes or stash/.test(stderr)) {
@@ -80,6 +83,22 @@ function runGitCommandWithAuth(args,cwd,auth) {
})
}
+function runGitCommandWithSSHCommand(args,cwd,auth) {
+ return sshResponseServer(auth).then(function(rs) {
+ var commandEnv = clone(process.env);
+ commandEnv.SSH_ASKPASS = path.join(__dirname,"node-red-ask-pass.sh");
+ commandEnv.DISPLAY = "dummy:0";
+ commandEnv.NODE_RED_GIT_NODE_PATH = process.execPath;
+ commandEnv.NODE_RED_GIT_SOCK_PATH = rs.path;
+ commandEnv.NODE_RED_GIT_ASKPASS_PATH = path.join(__dirname,"authWriter.js");
+ commandEnv.GIT_SSH_COMMAND = "ssh -i " + auth.key_path + " -F /dev/null";
+ // console.log('commandEnv:', commandEnv);
+ return runGitCommand(args,cwd,commandEnv).finally(function() {
+ rs.close();
+ });
+ })
+}
+
function cleanFilename(name) {
if (name[0] !== '"') {
return name;
@@ -362,7 +381,12 @@ module.exports = {
}
var promise;
if (auth) {
- promise = runGitCommandWithAuth(args,cwd,auth);
+ if ( auth.key_path ) {
+ promise = runGitCommandWithSSHCommand(args,cwd,auth);
+ }
+ else {
+ promise = runGitCommandWithAuth(args,cwd,auth);
+ }
} else {
promise = runGitCommand(args,cwd)
}
@@ -393,7 +417,12 @@ module.exports = {
args.push("--porcelain");
var promise;
if (auth) {
- promise = runGitCommandWithAuth(args,cwd,auth);
+ if ( auth.key_path ) {
+ promise = runGitCommandWithSSHCommand(args,cwd,auth);
+ }
+ else {
+ promise = runGitCommandWithAuth(args,cwd,auth);
+ }
} else {
promise = runGitCommand(args,cwd)
}
@@ -418,7 +447,12 @@ module.exports = {
}
args.push(".");
if (auth) {
- return runGitCommandWithAuth(args,cwd,auth);
+ if ( auth.key_path ) {
+ return runGitCommandWithSSHCommand(args,cwd,auth);
+ }
+ else {
+ return runGitCommandWithAuth(args,cwd,auth);
+ }
} else {
return runGitCommand(args,cwd);
}
@@ -477,7 +511,12 @@ module.exports = {
fetch: function(cwd,remote,auth) {
var args = ["fetch",remote];
if (auth) {
- return runGitCommandWithAuth(args,cwd,auth);
+ if ( auth.key_path ) {
+ return runGitCommandWithSSHCommand(args,cwd,auth);
+ }
+ else {
+ return runGitCommandWithAuth(args,cwd,auth);
+ }
} else {
return runGitCommand(args,cwd);
}
diff --git a/red/runtime/storage/localfilesystem/sshkeys.js b/red/runtime/storage/localfilesystem/sshkeys.js
new file mode 100644
index 000000000..4a5211cde
--- /dev/null
+++ b/red/runtime/storage/localfilesystem/sshkeys.js
@@ -0,0 +1,190 @@
+/**
+ * Copyright JS Foundation and other contributors, http://js.foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ **/
+
+var fs = require('fs-extra');
+var when = require('when');
+var fspath = require("path");
+var keygen = require('ssh-keygen');
+
+var settings;
+var runtime;
+var log;
+var sshkeyDir;
+
+function init(_settings, _runtime) {
+ settings = _settings;
+ runtime = _runtime;
+ log = runtime.log;
+ sshkeyDir = fspath.join(settings.userDir, "projects", ".sshkeys");
+ // console.log('sshkeys.init()');
+ return createSSHKeyDirectory();
+}
+
+function createSSHKeyDirectory() {
+ return fs.ensureDir(sshkeyDir);
+}
+
+function listSSHKeys(username) {
+ var startStr = username + '_';
+ // console.log('sshkeyDir:', sshkeyDir);
+ return fs.readdir(sshkeyDir).then(function(fns) {
+ var ret = fns.sort()
+ .filter(function(fn) {
+ var fullPath = fspath.join(sshkeyDir,fn);
+ if (fn.length > 2 || fn[0] != ".") {
+ var stats = fs.lstatSync(fullPath);
+ if (stats.isFile()) {
+ return fn.startsWith(startStr);
+ }
+ }
+ return false;
+ })
+ .map(function(filename) {
+ return filename.substr(startStr.length);
+ })
+ .reduce(function(prev, current) {
+ var parsePath = fspath.parse(current);
+ if ( parsePath ) {
+ if ( parsePath.ext !== '.pub' ) {
+ // Private Keys
+ prev.keyFiles.push(parsePath.base);
+ }
+ else if ( parsePath.ext === '.pub' && (prev.keyFiles.some(function(elem){ return elem === parsePath.name; }))) {
+ prev.privateKeyFiles.push(parsePath.name);
+ }
+ }
+ return prev;
+ }, { keyFiles: [], privateKeyFiles: [] });
+ return ret.privateKeyFiles.map(function(filename) {
+ return {
+ name: filename
+ };
+ });
+ });
+}
+
+function getSSHKey(username, name) {
+ return checkSSHKeyFileAndGetPublicKeyFileName(username, name)
+ .then(function(publicSSHKeyPath) {
+ return fs.readFile(publicSSHKeyPath, 'utf-8');
+ });
+}
+
+function generateSSHKey(username, options) {
+ options = options || {};
+ var name = options.name || "";
+ return checkExistSSHKeyFiles(username, name)
+ .then(function(result) {
+ if ( result ) {
+ throw new Error('Some SSH Keyfile exists');
+ }
+ else {
+ var email = options.email || "";
+ var password = options.password || "";
+ var size = options.size || 2048;
+ var sshKeyFileBasename = username + '_' + name;
+ var privateKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename);
+ return generateSSHKeyPair(privateKeyFilePath, email, password, size)
+ .then(function() {
+ return name;
+ });
+ }
+ })
+ .then(function(keyfile_name) {
+ return checkSSHKeyFileAndGetPublicKeyFileName(username, name)
+ .then(function() {
+ return keyfile_name;
+ })
+ .catch(function() {
+ throw new Error('Failed to generate ssh key files');
+ });
+ });
+}
+
+function deleteSSHKey(username, name) {
+ return checkSSHKeyFileAndGetPublicKeyFileName(username, name)
+ .then(function() {
+ return deleteSSHKeyFiles(username, name);
+ });
+}
+
+function checkExistSSHKeyFiles(username, name) {
+ var sshKeyFileBasename = username + '_' + name;
+ var privateKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename);
+ var publicKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename + '.pub');
+ return Promise.race([
+ fs.access(privateKeyFilePath, (fs.constants || fs).R_OK),
+ fs.access(publicKeyFilePath , (fs.constants || fs).R_OK)
+ ])
+ .then(function() {
+ return true;
+ })
+ .catch(function() {
+ return false;
+ });
+}
+
+function checkSSHKeyFileAndGetPublicKeyFileName(username, name) {
+ var sshKeyFileBasename = username + '_' + name;
+ var privateKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename);
+ var publicKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename + '.pub');
+ return Promise.all([
+ fs.access(privateKeyFilePath, (fs.constants || fs).R_OK),
+ fs.access(publicKeyFilePath , (fs.constants || fs).R_OK)
+ ])
+ .then(function() {
+ return publicKeyFilePath;
+ });
+}
+
+function deleteSSHKeyFiles(username, name) {
+ var sshKeyFileBasename = username + '_' + name;
+ var privateKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename);
+ var publicKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename + '.pub');
+ return Promise.all([
+ fs.remove(privateKeyFilePath),
+ fs.remove(publicKeyFilePath)
+ ])
+ .then(function(retArray) {
+ return true;
+ });
+}
+
+function generateSSHKeyPair(privateKeyPath, comment, password, size) {
+ return new Promise(function(resolve, reject) {
+ keygen({
+ location: privateKeyPath,
+ comment: comment,
+ password: password,
+ size: size
+ }, function(err, out) {
+ if ( err ) {
+ reject(err);
+ }
+ else {
+ resolve();
+ }
+ });
+ });
+}
+
+module.exports = {
+ init: init,
+ listSSHKeys: listSSHKeys,
+ getSSHKey: getSSHKey,
+ generateSSHKey: generateSSHKey,
+ deleteSSHKey: deleteSSHKey
+};
diff --git a/test/red/api/editor/sshkeys_spec.js b/test/red/api/editor/sshkeys_spec.js
new file mode 100644
index 000000000..ade1e7a07
--- /dev/null
+++ b/test/red/api/editor/sshkeys_spec.js
@@ -0,0 +1,355 @@
+/**
+ * Copyright JS Foundation and other contributors, http://js.foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ **/
+
+var should = require("should");
+var sinon = require("sinon");
+var request = require("supertest");
+var express = require("express");
+var editorApi = require("../../../../red/api/editor");
+var comms = require("../../../../red/api/editor/comms");
+var info = require("../../../../red/api/editor/settings");
+var auth = require("../../../../red/api/auth");
+var sshkeys = require("../../../../red/api/editor/sshkeys");
+var when = require("when");
+var bodyParser = require("body-parser");
+var fs = require("fs-extra");
+var fspath = require("path");
+
+
+describe("api/editor/sshkeys", function() {
+ var app;
+ var mockList = [
+ 'library','theme','locales','credentials','comms'
+ ]
+ var isStarted = true;
+ var errors = [];
+ var session_data = {};
+
+ var mockRuntime = {
+ settings:{
+ httpNodeRoot: true,
+ httpAdminRoot: true,
+ disableEditor: false,
+ exportNodeSettings:function(){},
+ storage: {
+ getSessions: function(){
+ return when.resolve(session_data);
+ },
+ setSessions: function(_session) {
+ session_data = _session;
+ return when.resolve();
+ }
+ },
+ log:{audit:function(){},error:function(msg){errors.push(msg)}}
+ },
+ storage: {
+ sshkeys: {
+ init: function(){},
+ listSSHKeys: function(){},
+ getSSHKey: function(){},
+ generateSSHKey: function(){},
+ deleteSSHKey: function(){},
+ }
+ },
+ events:{on:function(){},removeListener:function(){}},
+ isStarted: function() { return isStarted; },
+ nodes: {paletteEditorEnabled: function() { return false }}
+ };
+
+ before(function() {
+ auth.init(mockRuntime);
+ app = express();
+ app.use(bodyParser.json());
+ app.use(editorApi.init({},mockRuntime));
+ });
+ after(function() {
+ })
+
+ beforeEach(function() {
+ sinon.stub(mockRuntime.storage.sshkeys, "listSSHKeys");
+ sinon.stub(mockRuntime.storage.sshkeys, "getSSHKey");
+ sinon.stub(mockRuntime.storage.sshkeys, "generateSSHKey");
+ sinon.stub(mockRuntime.storage.sshkeys, "deleteSSHKey");
+ })
+ afterEach(function() {
+ mockRuntime.storage.sshkeys.listSSHKeys.restore();
+ mockRuntime.storage.sshkeys.getSSHKey.restore();
+ mockRuntime.storage.sshkeys.generateSSHKey.restore();
+ mockRuntime.storage.sshkeys.deleteSSHKey.restore();
+ })
+
+ it('GET /settings/user/keys --- return empty list', function(done) {
+ mockRuntime.storage.sshkeys.listSSHKeys.returns(Promise.resolve([]));
+ request(app)
+ .get("/settings/user/keys")
+ .expect(200)
+ .end(function(err,res) {
+ if (err) {
+ return done(err);
+ }
+ res.body.should.have.property('keys');
+ res.body.keys.should.be.empty();
+ done();
+ });
+ });
+
+ it('GET /settings/user/keys --- return normal list', function(done) {
+ var fileList = [
+ 'test_key01',
+ 'test_key02'
+ ];
+ var retList = fileList.map(function(elem) {
+ return {
+ name: elem
+ };
+ });
+ mockRuntime.storage.sshkeys.listSSHKeys.returns(Promise.resolve(retList));
+ request(app)
+ .get("/settings/user/keys")
+ .expect(200)
+ .end(function(err,res) {
+ if (err) {
+ return done(err);
+ }
+ res.body.should.have.property('keys');
+ for (var item of retList) {
+ res.body.keys.should.containEql(item);
+ }
+ done();
+ });
+ });
+
+ it('GET /settings/user/keys --- return Error', function(done) {
+ var errInstance = new Error("Messages.....");
+ errInstance.code = "test_code";
+ mockRuntime.storage.sshkeys.listSSHKeys.returns(Promise.reject(errInstance));
+ request(app)
+ .get("/settings/user/keys")
+ .expect(400)
+ .end(function(err,res) {
+ if (err) {
+ return done(err);
+ }
+ res.body.should.have.property('error');
+ res.body.error.should.be.equal(errInstance.code);
+ res.body.should.have.property('message');
+ res.body.message.should.be.equal(errInstance.message);
+ done();
+ });
+ });
+
+ it('GET /settings/user/keys --- return Unexpected Error', function(done) {
+ var errInstance = new Error("Messages.....");
+ mockRuntime.storage.sshkeys.listSSHKeys.returns(Promise.reject(errInstance));
+ request(app)
+ .get("/settings/user/keys")
+ .expect(400)
+ .end(function(err,res) {
+ if (err) {
+ return done(err);
+ }
+ res.body.should.have.property('error');
+ res.body.error.should.be.equal("unexpected_error");
+ res.body.should.have.property('message');
+ res.body.message.should.be.equal(errInstance.toString());
+ done();
+ });
+ });
+
+ it('GET /settings/user/keys/ --- return content', function(done) {
+ var key_file_name = "test_key";
+ var fileContent = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQD3a+sgtgzSbbliWxmOq5p6+H/mE+0gjWfLWrkIVmHENd1mifV4uCmIHAR2NfuadUYMQ3+bQ90kpmmEKTMYPsyentsKpHQZxTzG7wOCAIpJnbPTHDMxEJhVTaAwEjbVyMSIzTTPfnhoavWIBu0+uMgKDDlBm+RjlgkFlyhXyCN6UwFrIUUMH6Gw+eQHLiooKIl8ce7uDxIlt+9b7hFCU+sQ3kvuse239DZluu6+8buMWqJvrEHgzS9adRFKku8nSPAEPYn85vDi7OgVAcLQufknNgs47KHBAx9h04LeSrFJ/P5J1b//ItRpMOIme+O9d1BR46puzhvUaCHLdvO9czj+OmW+dIm+QIk6lZIOOMnppG72kZxtLfeKT16ur+2FbwAdL9ItBp4BI/YTlBPoa5mLMxpuWfmX1qHntvtGc9wEwS1P7YFfmF3XiK5apxalzrn0Qlr5UmDNbVIqJb1OlbC0w03Z0oktti1xT+R2DGOLWM4lBbpXDHV1BhQ7oYOvbUD8Cnof55lTP0WHHsOHlQc/BGDti1XA9aBX/OzVyzBUYEf0pkimsD0RYo6aqt7QwehJYdlz9x1NBguBffT0s4NhNb9IWr+ASnFPvNl2sw4XH/8U0J0q8ZkMpKkbLM1Zdp1Fv00GF0f5UNRokai6uM3w/ccantJ3WvZ6GtctqytWrw== \n";
+ mockRuntime.storage.sshkeys.getSSHKey.returns(Promise.resolve(fileContent));
+ request(app)
+ .get("/settings/user/keys/" + key_file_name)
+ .expect(200)
+ .end(function(err,res) {
+ if (err) {
+ return done(err);
+ }
+ mockRuntime.storage.sshkeys.getSSHKey.called.should.be.true();
+ res.body.should.be.deepEqual({ publickey: fileContent });
+ done();
+ });
+ });
+
+ it('GET /settings/user/keys/ --- return Error', function(done) {
+ var key_file_name = "test_key";
+ var errInstance = new Error("Messages.....");
+ errInstance.code = "test_code";
+ mockRuntime.storage.sshkeys.getSSHKey.returns(Promise.reject(errInstance));
+ request(app)
+ .get("/settings/user/keys/" + key_file_name)
+ .expect(400)
+ .end(function(err,res) {
+ if (err) {
+ return done(err);
+ }
+ res.body.should.have.property('error');
+ res.body.error.should.be.equal(errInstance.code);
+ res.body.should.have.property('message');
+ res.body.message.should.be.equal(errInstance.message);
+ done();
+ });
+ });
+
+ it('GET /settings/user/keys/ --- return Unexpected Error', function(done) {
+ var key_file_name = "test_key";
+ var errInstance = new Error("Messages.....");
+ mockRuntime.storage.sshkeys.getSSHKey.returns(Promise.reject(errInstance));
+ request(app)
+ .get("/settings/user/keys/" + key_file_name)
+ .expect(400)
+ .end(function(err,res) {
+ if (err) {
+ return done(err);
+ }
+ res.body.should.have.property('error');
+ res.body.error.should.be.equal("unexpected_error");
+ res.body.should.have.property('message');
+ res.body.message.should.be.equal(errInstance.toString());
+ done();
+ });
+ });
+
+ it('POST /settings/user/keys --- success', function(done) {
+ var key_file_name = "test_key";
+ mockRuntime.storage.sshkeys.generateSSHKey.returns(Promise.resolve(key_file_name));
+ request(app)
+ .post("/settings/user/keys")
+ .send({ name: key_file_name })
+ .expect(200)
+ .end(function(err,res) {
+ if (err) {
+ return done(err);
+ }
+ done();
+ });
+ });
+
+ it('POST /settings/user/keys --- return parameter error', function(done) {
+ var key_file_name = "test_key";
+ mockRuntime.storage.sshkeys.generateSSHKey.returns(Promise.resolve(key_file_name));
+ request(app)
+ .post("/settings/user/keys")
+ .expect(400)
+ .end(function(err,res) {
+ if (err) {
+ return done(err);
+ }
+ res.body.should.have.property('error');
+ res.body.error.should.be.equal("unexpected_error");
+ res.body.should.have.property('message');
+ res.body.message.should.be.equal("You need to have body or body.name");
+ done();
+ });
+ });
+
+ it('POST /settings/user/keys --- return Error', function(done) {
+ var key_file_name = "test_key";
+ var errInstance = new Error("Messages.....");
+ errInstance.code = "test_code";
+ mockRuntime.storage.sshkeys.generateSSHKey.returns(Promise.reject(errInstance));
+ request(app)
+ .post("/settings/user/keys")
+ .send({ name: key_file_name })
+ .expect(400)
+ .end(function(err,res) {
+ if (err) {
+ return done(err);
+ }
+ res.body.should.have.property('error');
+ res.body.error.should.be.equal("test_code");
+ res.body.should.have.property('message');
+ res.body.message.should.be.equal(errInstance.message);
+ done();
+ });
+ });
+
+ it('POST /settings/user/keys --- return Unexpected error', function(done) {
+ var key_file_name = "test_key";
+ var errInstance = new Error("Messages.....");
+ mockRuntime.storage.sshkeys.generateSSHKey.returns(Promise.reject(errInstance));
+ request(app)
+ .post("/settings/user/keys")
+ .send({ name: key_file_name })
+ .expect(400)
+ .end(function(err,res) {
+ if (err) {
+ return done(err);
+ }
+ res.body.should.have.property('error');
+ res.body.error.should.be.equal("unexpected_error");
+ res.body.should.have.property('message');
+ res.body.message.should.be.equal(errInstance.toString());
+ done();
+ });
+ });
+
+ it('DELETE /settings/user/keys/ --- success', function(done) {
+ var key_file_name = "test_key";
+ mockRuntime.storage.sshkeys.deleteSSHKey.returns(Promise.resolve(true));
+ request(app)
+ .delete("/settings/user/keys/" + key_file_name)
+ .expect(204)
+ .end(function(err,res) {
+ if (err) {
+ return done(err);
+ }
+ res.body.should.be.deepEqual({});
+ done();
+ });
+ });
+
+ it('DELETE /settings/user/keys/ --- return Error', function(done) {
+ var key_file_name = "test_key";
+ var errInstance = new Error("Messages.....");
+ errInstance.code = "test_code";
+ mockRuntime.storage.sshkeys.deleteSSHKey.returns(Promise.reject(errInstance));
+ request(app)
+ .delete("/settings/user/keys/" + key_file_name)
+ .expect(400)
+ .end(function(err,res) {
+ if (err) {
+ return done(err);
+ }
+ res.body.should.have.property('error');
+ res.body.error.should.be.equal("test_code");
+ res.body.should.have.property('message');
+ res.body.message.should.be.equal(errInstance.message);
+ done();
+ });
+ });
+
+ it('DELETE /settings/user/keys/ --- return Unexpected Error', function(done) {
+ var key_file_name = "test_key";
+ var errInstance = new Error("Messages.....");
+ mockRuntime.storage.sshkeys.deleteSSHKey.returns(Promise.reject(errInstance));
+ request(app)
+ .delete("/settings/user/keys/" + key_file_name)
+ .expect(400)
+ .end(function(err,res) {
+ if (err) {
+ return done(err);
+ }
+ res.body.should.have.property('error');
+ res.body.error.should.be.equal("unexpected_error");
+ res.body.should.have.property('message');
+ res.body.message.should.be.equal(errInstance.toString());
+ done();
+ });
+ });
+});
diff --git a/test/red/runtime/storage/localfilesystem/sshkeys_spec.js b/test/red/runtime/storage/localfilesystem/sshkeys_spec.js
new file mode 100644
index 000000000..370ba9908
--- /dev/null
+++ b/test/red/runtime/storage/localfilesystem/sshkeys_spec.js
@@ -0,0 +1,424 @@
+/**
+ * Copyright JS Foundation and other contributors, http://js.foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ **/
+var should = require("should");
+var fs = require('fs-extra');
+var path = require('path');
+
+var localfilesystem = require("../../../../../red/runtime/storage/localfilesystem");
+var sshkeys = require("../../../../../red/runtime/storage/localfilesystem/sshkeys");
+
+describe("storage/localfilesystem/sshkeys", function() {
+ var userDir = path.join(__dirname,".testSSHKeyUserHome");
+ var mockSettings = {
+ userDir: userDir
+ };
+ var mockRuntime = {
+ log:{
+ _:function() { return "placeholder message"},
+ info: function() { },
+ trace: function() { }
+ }
+ };
+ beforeEach(function(done) {
+ fs.remove(userDir,function(err) {
+ fs.mkdir(userDir,done);
+ });
+ });
+ afterEach(function(done) {
+ fs.remove(userDir,done);
+ });
+
+ it('should create sshkey directory when sshkey initializes', function(done) {
+ var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys');
+ localfilesystem.init(mockSettings, mockRuntime).then(function() {
+ sshkeys.init(mockSettings, mockRuntime).then(function() {
+ var ret = fs.existsSync(sshkeyDirPath);
+ fs.existsSync(sshkeyDirPath).should.be.true();
+ done();
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ });
+
+ it('should get sshkey empty list if there is no sshkey file', function(done) {
+ var username = 'test';
+ localfilesystem.init(mockSettings, mockRuntime).then(function() {
+ sshkeys.init(mockSettings, mockRuntime).then(function() {
+ sshkeys.listSSHKeys(username).then(function(retObj) {
+ console.log('retObj:', retObj);
+ retObj.should.be.instanceOf(Array).and.have.lengthOf(0);
+ done();
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ });
+
+ it('should get sshkey list', function(done) {
+ var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys');
+ var username = 'test';
+ var filenameList = ['test-key01', 'test-key02'];
+ localfilesystem.init(mockSettings, mockRuntime).then(function() {
+ sshkeys.init(mockSettings, mockRuntime).then(function() {
+ for(var filename of filenameList) {
+ fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename),"","utf8");
+ fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename+".pub"),"","utf8");
+ }
+ sshkeys.listSSHKeys(username).then(function(retObj) {
+ retObj.should.be.instanceOf(Array).and.have.lengthOf(filenameList.length);
+ for(var filename of filenameList) {
+ retObj.should.containEql({ name: filename });
+ }
+ done();
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ });
+
+ it('should not get sshkey file if there is only private key', function(done) {
+ var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys');
+ var username = 'test';
+ var filenameList = ['test-key01', 'test-key02'];
+ var onlyPrivateKeyFilenameList = ['test-key03', 'test-key04'];
+ localfilesystem.init(mockSettings, mockRuntime).then(function() {
+ sshkeys.init(mockSettings, mockRuntime).then(function() {
+ for(var filename of filenameList) {
+ fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename),"","utf8");
+ fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename+".pub"),"","utf8");
+ }
+ for(var filename of onlyPrivateKeyFilenameList) {
+ fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename),"","utf8");
+ }
+ sshkeys.listSSHKeys(username).then(function(retObj) {
+ retObj.should.be.instanceOf(Array).and.have.lengthOf(filenameList.length);
+ for(var filename of filenameList) {
+ retObj.should.containEql({ name: filename });
+ }
+ for(var filename of onlyPrivateKeyFilenameList) {
+ retObj.should.not.containEql({ name: filename });
+ }
+ done();
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ });
+
+ it('should not get sshkey file if there is only public key', function(done) {
+ var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys');
+ var username = 'test';
+ var filenameList = ['test-key01', 'test-key02'];
+ var directoryList = ['test-key03', '.test-key04'];
+ localfilesystem.init(mockSettings, mockRuntime).then(function() {
+ sshkeys.init(mockSettings, mockRuntime).then(function() {
+ for(var filename of filenameList) {
+ fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename),"","utf8");
+ fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename+".pub"),"","utf8");
+ }
+ for(var filename of directoryList) {
+ fs.ensureDirSync(path.join(sshkeyDirPath,filename));
+ }
+ sshkeys.listSSHKeys(username).then(function(retObj) {
+ retObj.should.be.instanceOf(Array).and.have.lengthOf(filenameList.length);
+ for(var filename of filenameList) {
+ retObj.should.containEql({ name: filename });
+ }
+ for(var directoryname of directoryList) {
+ retObj.should.not.containEql({ name: directoryname });
+ }
+ done();
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ });
+
+ it('should get sshkey list that does not have directory', function(done) {
+ var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys');
+ var username = 'test';
+ var otherUsername = 'other';
+ var filenameList = ['test-key01', 'test-key02'];
+ var otherUserFilenameList = ['test-key03', 'test-key04'];
+ localfilesystem.init(mockSettings, mockRuntime).then(function() {
+ sshkeys.init(mockSettings, mockRuntime).then(function() {
+ for(var filename of filenameList) {
+ fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename),"","utf8");
+ fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename+".pub"),"","utf8");
+ }
+ for(var filename of otherUserFilenameList) {
+ fs.writeFileSync(path.join(sshkeyDirPath,otherUsername+"_"+filename),"","utf8");
+ fs.writeFileSync(path.join(sshkeyDirPath,otherUsername+"_"+filename+".pub"),"","utf8");
+ }
+ sshkeys.listSSHKeys(username).then(function(retObj) {
+ retObj.should.be.instanceOf(Array).and.have.lengthOf(filenameList.length);
+ for(var filename of filenameList) {
+ retObj.should.containEql({ name: filename });
+ }
+ for(var filename of otherUserFilenameList) {
+ retObj.should.not.containEql({ name: filename });
+ }
+ done();
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ });
+
+ it('should get sshkey list that have keys of specified user', function(done) {
+ var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys');
+ var username = 'test';
+ var otherUsername = 'other';
+ var filenameList = ['test-key01', 'test-key02'];
+ var otherUserFilenameList = ['test-key03', 'test-key04'];
+ localfilesystem.init(mockSettings, mockRuntime).then(function() {
+ sshkeys.init(mockSettings, mockRuntime).then(function() {
+ for(var filename of filenameList) {
+ fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename),"","utf8");
+ fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename+".pub"),"","utf8");
+ }
+ for(var filename of otherUserFilenameList) {
+ fs.writeFileSync(path.join(sshkeyDirPath,otherUsername+"_"+filename),"","utf8");
+ fs.writeFileSync(path.join(sshkeyDirPath,otherUsername+"_"+filename+".pub"),"","utf8");
+ }
+ sshkeys.listSSHKeys(username).then(function(retObj) {
+ retObj.should.be.instanceOf(Array).and.have.lengthOf(filenameList.length);
+ for(var filename of filenameList) {
+ retObj.should.containEql({ name: filename });
+ }
+ for(var filename of otherUserFilenameList) {
+ retObj.should.not.containEql({ name: filename });
+ }
+ done();
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ });
+
+ it('should generate sshkey file with empty data', function(done) {
+ var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys');
+ var username = 'test';
+ var options = {
+ email: 'test@test.com',
+ name: 'test-key01'
+ };
+ localfilesystem.init(mockSettings, mockRuntime).then(function() {
+ sshkeys.init(mockSettings, mockRuntime).then(function() {
+ sshkeys.generateSSHKey(username, options).then(function(retObj) {
+ retObj.should.be.equal(options.name);
+ fs.existsSync(path.join(sshkeyDirPath,username+'_'+options.name)).should.be.true();
+ fs.existsSync(path.join(sshkeyDirPath,username+'_'+options.name+'.pub')).should.be.true();
+ done();
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ });
+
+ it('should generate sshkey file with password data', function(done) {
+ var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys');
+ var username = 'test';
+ var options = {
+ email: 'test@test.com',
+ name: 'test-key01',
+ password: 'testtest'
+ };
+ localfilesystem.init(mockSettings, mockRuntime).then(function() {
+ sshkeys.init(mockSettings, mockRuntime).then(function() {
+ sshkeys.generateSSHKey(username, options).then(function(retObj) {
+ retObj.should.be.equal(options.name);
+ fs.existsSync(path.join(sshkeyDirPath,username+'_'+options.name)).should.be.true();
+ fs.existsSync(path.join(sshkeyDirPath,username+'_'+options.name+'.pub')).should.be.true();
+ done();
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ });
+
+ it('should generate sshkey file with size data', function(done) {
+ var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys');
+ var username = 'test';
+ var options = {
+ email: 'test@test.com',
+ name: 'test-key01',
+ size: 4096
+ };
+ localfilesystem.init(mockSettings, mockRuntime).then(function() {
+ sshkeys.init(mockSettings, mockRuntime).then(function() {
+ sshkeys.generateSSHKey(username, options).then(function(retObj) {
+ retObj.should.be.equal(options.name);
+ fs.existsSync(path.join(sshkeyDirPath,username+'_'+options.name)).should.be.true();
+ fs.existsSync(path.join(sshkeyDirPath,username+'_'+options.name+'.pub')).should.be.true();
+ done();
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ });
+
+ it('should generate sshkey file with password & size data', function(done) {
+ this.timeout(5000);
+ var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys');
+ var username = 'test';
+ var options = {
+ email: 'test@test.com',
+ name: 'test-key01',
+ password: 'testtest',
+ size: 4096
+ };
+ localfilesystem.init(mockSettings, mockRuntime).then(function() {
+ sshkeys.init(mockSettings, mockRuntime).then(function() {
+ sshkeys.generateSSHKey(username, options).then(function(retObj) {
+ retObj.should.be.equal(options.name);
+ fs.existsSync(path.join(sshkeyDirPath,username+'_'+options.name)).should.be.true();
+ fs.existsSync(path.join(sshkeyDirPath,username+'_'+options.name+'.pub')).should.be.true();
+ done();
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ });
+
+ it('should not generate sshkey file with illegal size data', function(done) {
+ this.timeout(5000);
+ var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys');
+ var username = 'test';
+ var options = {
+ email: 'test@test.com',
+ name: 'test-key01',
+ size: 3333
+ };
+ localfilesystem.init(mockSettings, mockRuntime).then(function() {
+ sshkeys.init(mockSettings, mockRuntime).then(function() {
+ sshkeys.generateSSHKey(username, options).then(function(retObj) {
+ retObj.should.be.equal(options.name);
+ fs.existsSync(path.join(sshkeyDirPath,username+'_'+options.name)).should.be.true();
+ fs.existsSync(path.join(sshkeyDirPath,username+'_'+options.name+'.pub')).should.be.true();
+ done();
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ });
+
+ it('should get sshkey file content', function(done) {
+ var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys');
+ var username = 'test';
+ var filename = 'test-key01';
+ var fileContent = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQD3a+sgtgzSbbliWxmOq5p6+H/mE+0gjWfLWrkIVmHENd1mifV4uCmIHAR2NfuadUYMQ3+bQ90kpmmEKTMYPsyentsKpHQZxTzG7wOCAIpJnbPTHDMxEJhVTaAwEjbVyMSIzTTPfnhoavWIBu0+uMgKDDlBm+RjlgkFlyhXyCN6UwFrIUUMH6Gw+eQHLiooKIl8ce7uDxIlt+9b7hFCU+sQ3kvuse239DZluu6+8buMWqJvrEHgzS9adRFKku8nSPAEPYn85vDi7OgVAcLQufknNgs47KHBAx9h04LeSrFJ/P5J1b//ItRpMOIme+O9d1BR46puzhvUaCHLdvO9czj+OmW+dIm+QIk6lZIOOMnppG72kZxtLfeKT16ur+2FbwAdL9ItBp4BI/YTlBPoa5mLMxpuWfmX1qHntvtGc9wEwS1P7YFfmF3XiK5apxalzrn0Qlr5UmDNbVIqJb1OlbC0w03Z0oktti1xT+R2DGOLWM4lBbpXDHV1BhQ7oYOvbUD8Cnof55lTP0WHHsOHlQc/BGDti1XA9aBX/OzVyzBUYEf0pkimsD0RYo6aqt7QwehJYdlz9x1NBguBffT0s4NhNb9IWr+ASnFPvNl2sw4XH/8U0J0q8ZkMpKkbLM1Zdp1Fv00GF0f5UNRokai6uM3w/ccantJ3WvZ6GtctqytWrw== \n";
+ localfilesystem.init(mockSettings, mockRuntime).then(function() {
+ sshkeys.init(mockSettings, mockRuntime).then(function() {
+ fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename),"","utf8");
+ fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename+".pub"),fileContent,"utf8");
+ sshkeys.getSSHKey(username, filename).then(function(retObj) {
+ retObj.should.be.equal(fileContent);
+ done();
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ });
+
+ it('should delete sshkey files', function(done) {
+ var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys');
+ var username = 'test';
+ var filename = 'test-key01';
+ var fileContent = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQD3a+sgtgzSbbliWxmOq5p6+H/mE+0gjWfLWrkIVmHENd1mifV4uCmIHAR2NfuadUYMQ3+bQ90kpmmEKTMYPsyentsKpHQZxTzG7wOCAIpJnbPTHDMxEJhVTaAwEjbVyMSIzTTPfnhoavWIBu0+uMgKDDlBm+RjlgkFlyhXyCN6UwFrIUUMH6Gw+eQHLiooKIl8ce7uDxIlt+9b7hFCU+sQ3kvuse239DZluu6+8buMWqJvrEHgzS9adRFKku8nSPAEPYn85vDi7OgVAcLQufknNgs47KHBAx9h04LeSrFJ/P5J1b//ItRpMOIme+O9d1BR46puzhvUaCHLdvO9czj+OmW+dIm+QIk6lZIOOMnppG72kZxtLfeKT16ur+2FbwAdL9ItBp4BI/YTlBPoa5mLMxpuWfmX1qHntvtGc9wEwS1P7YFfmF3XiK5apxalzrn0Qlr5UmDNbVIqJb1OlbC0w03Z0oktti1xT+R2DGOLWM4lBbpXDHV1BhQ7oYOvbUD8Cnof55lTP0WHHsOHlQc/BGDti1XA9aBX/OzVyzBUYEf0pkimsD0RYo6aqt7QwehJYdlz9x1NBguBffT0s4NhNb9IWr+ASnFPvNl2sw4XH/8U0J0q8ZkMpKkbLM1Zdp1Fv00GF0f5UNRokai6uM3w/ccantJ3WvZ6GtctqytWrw== \n";
+ localfilesystem.init(mockSettings, mockRuntime).then(function() {
+ sshkeys.init(mockSettings, mockRuntime).then(function() {
+ fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename),"","utf8");
+ fs.writeFileSync(path.join(sshkeyDirPath,username+"_"+filename+".pub"),fileContent,"utf8");
+ sshkeys.deleteSSHKey(username, filename).then(function(retObj) {
+ retObj.should.be.true();
+ fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename)).should.be.false();
+ fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename+'.pub')).should.be.false();
+ done();
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ }).catch(function(err) {
+ done(err);
+ });
+ });
+});