From 923893e16066bc2b7df9de62b98fb3191320ae9b Mon Sep 17 00:00:00 2001 From: Hideki Nakamura Date: Thu, 7 Dec 2017 23:11:24 +0900 Subject: [PATCH 01/12] Add SSH key management API --- package.json | 98 ++-- red/api/editor/index.js | 4 + red/api/editor/sshkeys.js | 125 ++++++ red/runtime/storage/index.js | 3 + red/runtime/storage/localfilesystem/index.js | 5 +- .../storage/localfilesystem/sshkeys.js | 152 +++++++ test/red/api/editor/sshkeys_spec.js | 377 ++++++++++++++++ .../storage/localfilesystem/sshkeys_spec.js | 422 ++++++++++++++++++ 8 files changed, 1140 insertions(+), 46 deletions(-) create mode 100644 red/api/editor/sshkeys.js create mode 100644 red/runtime/storage/localfilesystem/sshkeys.js create mode 100644 test/red/api/editor/sshkeys_spec.js create mode 100644 test/red/runtime/storage/localfilesystem/sshkeys_spec.js diff --git a/package.json b/package.json index 044d485de..4e18bad23 100644 --- a/package.json +++ b/package.json @@ -1,92 +1,100 @@ { - "name" : "node-red", - "version" : "0.17.5", - "description" : "A visual tool for wiring the Internet of Things", - "homepage" : "http://nodered.org", - "license" : "Apache-2.0", - "repository" : { - "type":"git", - "url":"https://github.com/node-red/node-red.git" + "name": "node-red", + "version": "0.17.5", + "description": "A visual tool for wiring the Internet of Things", + "homepage": "http://nodered.org", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/node-red/node-red.git" }, - "main" : "red/red.js", - "scripts" : { + "main": "red/red.js", + "scripts": { "start": "node red.js", "test": "grunt", "build": "grunt build" }, - "bin" : { + "bin": { "node-red": "./red.js", "node-red-pi": "bin/node-red-pi" }, "contributors": [ - {"name": "Nick O'Leary"}, - {"name": "Dave Conway-Jones"} + { + "name": "Nick O'Leary" + }, + { + "name": "Dave Conway-Jones" + } ], "keywords": [ - "editor", "messaging", "iot", "flow" + "editor", + "messaging", + "iot", + "flow" ], "dependencies": { "basic-auth": "1.1.0", "bcryptjs": "2.4.3", "body-parser": "1.17.2", - "cheerio":"0.22.0", + "cheerio": "0.22.0", "clone": "2.1.1", "cookie": "0.3.1", "cookie-parser": "1.4.3", - "cors":"2.8.3", - "cron":"1.2.1", + "cors": "2.8.3", + "cron": "1.2.1", "express": "4.15.3", "express-session": "1.15.2", - "follow-redirects":"1.2.4", + "follow-redirects": "1.2.4", "fs-extra": "4.0.2", - "fs.notify":"0.0.4", - "hash-sum":"1.0.2", - "i18next":"1.10.6", - "is-utf8":"0.2.1", + "fs.notify": "0.0.4", + "hash-sum": "1.0.2", + "i18next": "1.10.6", + "is-utf8": "0.2.1", "js-yaml": "3.8.4", - "json-stringify-safe":"5.0.1", - "jsonata":"1.2.6", + "json-stringify-safe": "5.0.1", + "jsonata": "1.2.6", "media-typer": "0.3.0", "mqtt": "2.9.0", "multer": "1.3.0", "mustache": "2.3.0", + "node-red-node-email": "0.1.*", + "node-red-node-feedparser": "0.1.*", + "node-red-node-rbe": "0.1.*", + "node-red-node-twitter": "0.1.*", "nopt": "3.0.6", - "oauth2orize":"1.8.0", - "on-headers":"1.0.1", - "passport":"0.3.2", - "passport-http-bearer":"1.0.1", - "passport-oauth2-client-password":"0.1.2", - "raw-body":"2.2.0", + "oauth2orize": "1.8.0", + "on-headers": "1.0.1", + "passport": "0.3.2", + "passport-http-bearer": "1.0.1", + "passport-oauth2-client-password": "0.1.2", + "raw-body": "2.2.0", "semver": "5.3.0", - "sentiment":"2.1.0", - "uglify-js":"3.0.20", + "sentiment": "2.1.0", + "ssh-keygen": "^0.4.1", + "uglify-js": "3.0.20", "when": "3.7.8", "ws": "1.1.1", - "xml2js":"0.4.17", - "node-red-node-feedparser":"0.1.*", - "node-red-node-email":"0.1.*", - "node-red-node-twitter":"0.1.*", - "node-red-node-rbe":"0.1.*" + "xml2js": "0.4.17" }, "optionalDependencies": { - "bcrypt":"~1.0.1" + "bcrypt": "~1.0.1" }, "devDependencies": { "grunt": "~1.0.1", "grunt-chmod": "~1.1.1", "grunt-cli": "~1.2.0", - "grunt-concurrent":"~2.3.1", - "grunt-contrib-clean":"~1.1.0", + "grunt-concurrent": "~2.3.1", + "grunt-contrib-clean": "~1.1.0", "grunt-contrib-compress": "~1.4.0", - "grunt-contrib-concat":"~1.0.1", + "grunt-contrib-concat": "~1.0.1", "grunt-contrib-copy": "~1.0.0", "grunt-contrib-jshint": "~1.1.0", "grunt-contrib-uglify": "~3.0.1", - "grunt-contrib-watch":"~1.0.0", - "grunt-jsonlint":"~1.1.0", + "grunt-contrib-watch": "~1.0.0", + "grunt-jsonlint": "~1.1.0", "grunt-mocha-istanbul": "5.0.2", - "grunt-nodemon":"~0.4.2", - "grunt-sass":"~1.2.1", + "grunt-nodemon": "~0.4.2", + "grunt-sass": "~1.2.1", "grunt-simple-mocha": "~0.4.1", "istanbul": "0.4.5", "mocha": "~3.4.2", 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..5a9506555 --- /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 = os.hostname(); + 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.name, 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 f60862fe7..f9aba118f 100644 --- a/red/runtime/storage/index.js +++ b/red/runtime/storage/index.js @@ -57,6 +57,9 @@ var storageModuleInterface = { if (storageModule.projects) { 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/sshkeys.js b/red/runtime/storage/localfilesystem/sshkeys.js new file mode 100644 index 000000000..abb92a3cd --- /dev/null +++ b/red/runtime/storage/localfilesystem/sshkeys.js @@ -0,0 +1,152 @@ +/** + * 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[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, email, name, data) { + var sshKeyFileBasename = username + '_' + name; + var privateKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename); + return generateSSHKeyPair(privateKeyFilePath, email, data.password, data.size) + .then(function() { + return name; + }); +} + +function deleteSSHKey(username, name) { + return checkSSHKeyFileAndGetPublicKeyFileName(username, name) + .then(function() { + return deleteSSHKeyFiles(username, name); + }); +} + +function checkSSHKeyFileAndGetPublicKeyFileName(username, name) { + var sshKeyFileBasename = username + '_' + name; + var privateKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename); + var publicKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename + '.pub'); + return when.all([ + fs.access(privateKeyFilePath, (fs.constants || fs).R_OK), + fs.access(publicKeyFilePath , (fs.constants || fs).R_OK) + ]) + .then(function() { + return when.resolve(publicKeyFilePath); + }); +} + +function deleteSSHKeyFiles(username, name) { + var sshKeyFileBasename = username + '_' + name; + var privateKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename); + var publicKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename + '.pub'); + return when.all([ + fs.remove(privateKeyFilePath), + fs.remove(publicKeyFilePath) + ]) + .then(function(retArray) { + return when.resolve(true); + }); +} + +function generateSSHKeyPair(privateKeyPath, comment, password, size) { + return when.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..68f8784be --- /dev/null +++ b/test/red/api/editor/sshkeys_spec.js @@ -0,0 +1,377 @@ +/** + * 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 = {}; + // before(function() { + // mockList.forEach(function(m) { + // sinon.stub(require("../../../../../red/api/editor/"+m),"init",function(){}); + // }); + // sinon.stub(require("../../../../../red/api/editor/theme"),"app",function(){ return express()}); + // }); + // after(function() { + // mockList.forEach(function(m) { + // require("../../../../../red/api/editor/"+m).init.restore(); + // }) + // require("../../../../../red/api/editor/theme").app.restore(); + // }); + var mockRuntime = { + settings:{ + httpNodeRoot: true, + httpAdminRoot: true, + disableEditor: false, + exportNodeSettings:function(){}, + // adminAuth: { + // default: { + // permissions: ['*'] + // } + // }, + 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() { + // fs.removeSync() + }) + + 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....."); + // 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("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....."); + // 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("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") + // .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("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....."); + // 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("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.true(); + 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....."); + // 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("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..8f6619414 --- /dev/null +++ b/test/red/runtime/storage/localfilesystem/sshkeys_spec.js @@ -0,0 +1,422 @@ +/** + * 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() { } + } + }; + 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 email = 'test@test.com'; + var filename = 'test-key01'; + var data = {}; + localfilesystem.init(mockSettings, mockRuntime).then(function() { + sshkeys.init(mockSettings, mockRuntime).then(function() { + sshkeys.generateSSHKey(username, email, filename, data).then(function(retObj) { + retObj.should.be.equal(filename); + fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename)).should.be.true(); + fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename+'.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 email = 'test@test.com'; + var filename = 'test-key01'; + var data = { + password: 'testtest' + }; + localfilesystem.init(mockSettings, mockRuntime).then(function() { + sshkeys.init(mockSettings, mockRuntime).then(function() { + sshkeys.generateSSHKey(username, email, filename, data).then(function(retObj) { + retObj.should.be.equal(filename); + fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename)).should.be.true(); + fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename+'.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 email = 'test@test.com'; + var filename = 'test-key01'; + var data = { + size: 4096 + }; + localfilesystem.init(mockSettings, mockRuntime).then(function() { + sshkeys.init(mockSettings, mockRuntime).then(function() { + sshkeys.generateSSHKey(username, email, filename, data).then(function(retObj) { + retObj.should.be.equal(filename); + fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename)).should.be.true(); + fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename+'.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 email = 'test@test.com'; + var filename = 'test-key01'; + var data = { + password: 'testtest', + size: 4096 + }; + localfilesystem.init(mockSettings, mockRuntime).then(function() { + sshkeys.init(mockSettings, mockRuntime).then(function() { + sshkeys.generateSSHKey(username, email, filename, data).then(function(retObj) { + retObj.should.be.equal(filename); + fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename)).should.be.true(); + fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename+'.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 email = 'test@test.com'; + var filename = 'test-key01'; + var data = { + size: 3333 + }; + localfilesystem.init(mockSettings, mockRuntime).then(function() { + sshkeys.init(mockSettings, mockRuntime).then(function() { + sshkeys.generateSSHKey(username, email, filename, data).then(function(retObj) { + retObj.should.be.equal(filename); + fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename)).should.be.true(); + fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename+'.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); + }); + }); +}); From d1106f53e01253fdaeb08089d710c2cd873fc830 Mon Sep 17 00:00:00 2001 From: Hideki Nakamura Date: Fri, 15 Dec 2017 21:41:14 +0900 Subject: [PATCH 02/12] Pass email data into the SSH Key generation API --- red/api/editor/sshkeys.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/red/api/editor/sshkeys.js b/red/api/editor/sshkeys.js index 5a9506555..a7b3a9181 100644 --- a/red/api/editor/sshkeys.js +++ b/red/api/editor/sshkeys.js @@ -82,7 +82,7 @@ module.exports = { var username = getUsername(req.user); console.log('req.body:', req.body); if ( req.body && req.body.name ) { - runtime.storage.sshkeys.generateSSHKey(username, "", req.body.name, req.body) + runtime.storage.sshkeys.generateSSHKey(username, req.body.email || "", req.body.name, req.body) .then(function(name) { console.log('generate key --- success name:', name); res.json({ From 3a311c9584a5ab18e79cb61e56b9add3f928be4a Mon Sep 17 00:00:00 2001 From: Hideki Nakamura Date: Fri, 15 Dec 2017 23:48:52 +0900 Subject: [PATCH 03/12] Detect a SSH key generation error --- .../storage/localfilesystem/sshkeys.js | 55 +++++++++++++++---- test/red/api/editor/sshkeys_spec.js | 2 +- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/red/runtime/storage/localfilesystem/sshkeys.js b/red/runtime/storage/localfilesystem/sshkeys.js index abb92a3cd..96097aeba 100644 --- a/red/runtime/storage/localfilesystem/sshkeys.js +++ b/red/runtime/storage/localfilesystem/sshkeys.js @@ -84,11 +84,28 @@ function getSSHKey(username, name) { } function generateSSHKey(username, email, name, data) { - var sshKeyFileBasename = username + '_' + name; - var privateKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename); - return generateSSHKeyPair(privateKeyFilePath, email, data.password, data.size) - .then(function() { - return name; + return checkExistSSHKeyFiles(username, name) + .then(function(result) { + if ( result ) { + throw new Error('Some SSH Keyfile exists'); + } + else { + var sshKeyFileBasename = username + '_' + name; + var privateKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename); + return generateSSHKeyPair(privateKeyFilePath, email, data.password, data.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'); + }); }); } @@ -99,16 +116,32 @@ function deleteSSHKey(username, name) { }); } -function checkSSHKeyFileAndGetPublicKeyFileName(username, name) { +function checkExistSSHKeyFiles(username, name) { var sshKeyFileBasename = username + '_' + name; var privateKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename); var publicKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename + '.pub'); - return when.all([ + return Promise.race([ fs.access(privateKeyFilePath, (fs.constants || fs).R_OK), fs.access(publicKeyFilePath , (fs.constants || fs).R_OK) ]) .then(function() { - return when.resolve(publicKeyFilePath); + 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; }); } @@ -116,17 +149,17 @@ function deleteSSHKeyFiles(username, name) { var sshKeyFileBasename = username + '_' + name; var privateKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename); var publicKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename + '.pub'); - return when.all([ + return Promise.all([ fs.remove(privateKeyFilePath), fs.remove(publicKeyFilePath) ]) .then(function(retArray) { - return when.resolve(true); + return true; }); } function generateSSHKeyPair(privateKeyPath, comment, password, size) { - return when.promise(function(resolve, reject) { + return new Promise(function(resolve, reject) { keygen({ location: privateKeyPath, comment: comment, diff --git a/test/red/api/editor/sshkeys_spec.js b/test/red/api/editor/sshkeys_spec.js index 68f8784be..31ed6f865 100644 --- a/test/red/api/editor/sshkeys_spec.js +++ b/test/red/api/editor/sshkeys_spec.js @@ -330,7 +330,7 @@ describe("api/editor/sshkeys", function() { if (err) { return done(err); } - res.body.should.be.true(); + res.body.should.be.deepEqual({}); done(); }); }); From fe10b8650f8ebf0d5dd334af30b8bbcf7047ac13 Mon Sep 17 00:00:00 2001 From: Hideki Nakamura Date: Sat, 16 Dec 2017 00:07:47 +0900 Subject: [PATCH 04/12] Add Git access feature via SSH and Enhance SSH Key management --- editor/js/ui/projectUserSettings.js | 299 ++++++++++++++---- editor/js/ui/projects.js | 146 ++++++++- editor/sass/projects.scss | 88 ++++++ .../localfilesystem/projects/Project.js | 11 + .../projects/git/authServer.js | 50 ++- .../localfilesystem/projects/git/index.js | 59 +++- .../storage/localfilesystem/projects/index.js | 2 +- 7 files changed, 571 insertions(+), 84 deletions(-) diff --git a/editor/js/ui/projectUserSettings.js b/editor/js/ui/projectUserSettings.js index 2401e5985..afb111134 100644 --- a/editor/js/ui/projectUserSettings.js +++ b/editor/js/ui/projectUserSettings.js @@ -16,6 +16,7 @@ RED.projects.userSettings = (function() { + var gitconfigContainer; var gitUsernameInput; var gitEmailInput; @@ -24,11 +25,9 @@ RED.projects.userSettings = (function() { var currentGitSettings = RED.settings.get('git') || {}; currentGitSettings.user = currentGitSettings.user || {}; - - var title = $('

').text("Committer Details").appendTo(pane); - var gitconfigContainer = $('').appendTo(pane); + gitconfigContainer = $('').appendTo(pane); $('
').appendTo(gitconfigContainer).text("Leave blank to use system default"); var row = $('').appendTo(gitconfigContainer); @@ -40,77 +39,245 @@ RED.projects.userSettings = (function() { $('').text('Email').appendTo(row); gitEmailInput = $('').appendTo(row); gitEmailInput.val(currentGitSettings.user.email||""); - // var sshkeyTitle = $('

').text("SSH Keys").appendTo(gitconfigContainer); - // var generateSshKeyButton = $('') - // .appendTo(sshkeyTitle) - // .click(function(evt) { - // console.log('click generateSshKeyButton'); - // }); - // row = $('').appendTo(gitconfigContainer); - // var sshkeysList = $('
    ').appendTo(row); - // sshkeysList.editableList({ - // addButton: false, - // height: 'auto', - // addItem: function(outer,index,entry) { + var sshkeyTitle = $('

    ').text("SSH Keys").appendTo(gitconfigContainer); + var editSshKeyListButton = $('') + .appendTo(sshkeyTitle) + .click(function(evt) { + editSshKeyListButton.hide(); + formButtons.show(); + sshkeyInputRow.show(); + $(".projects-dialog-sshkey-list-button-remove").css('display', 'inline-block'); + }); + + var sshkeyListOptions = { + height: "300px", + deleteAction: function(entry, header) { + sendSSHKeyManagementAPI("DELETE_KEY", entry.name, function(data) { + hideSSHKeyGenerateForm(); + utils.refreshSSHKeyList(sshkeysList); + }); + }, + selectAction: function(entry, header) { + sendSSHKeyManagementAPI("GET_KEY_DETAIL", entry.name, function(data) { + setDialogContext(entry.name, data.publickey); + dialog.dialog("open"); + }); + } + }; + var sshkeysListRow = $('').appendTo(gitconfigContainer); + var sshkeysList = utils.createSSHKeyList(sshkeyListOptions).appendTo(sshkeysListRow); - // var header = $('
    ').appendTo(outer); - // entry.header = $('').text(entry.path||"Add new remote").appendTo(header); - // var body = $('
    ').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 + }, + 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, 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 done = function(err) { + 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 0493bad0a..e6a71f728 100644 --- a/editor/js/ui/projects.js +++ b/editor/js/ui/projects.js @@ -54,6 +54,7 @@ RED.projects = (function() { var projectRepoUserInput; var projectRepoPasswordInput; var projectNameSublabel; + var projectRepoSSHKeySelect; var projectRepoPassphrase; var projectRepoRemoteName var projectRepoBranch; @@ -126,12 +127,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(); } @@ -331,6 +338,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); @@ -397,14 +414,29 @@ 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)) { + projectData.git = { + remotes: { + 'origin': { + url: repoUrl, + key_file: getSelectedSSHKey(projectRepoSSHKeySelect).name, + passphrase: projectRepoPassphrase.val() + } } - } + }; + } + else { + projectData.git = { + remotes: { + 'origin': { + url: repoUrl, + username: projectRepoUserInput.val(), + password: projectRepoPasswordInput.val() + } + } + }; } } @@ -434,6 +466,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) { @@ -694,6 +728,100 @@ RED.projects = (function() { return container; } + // var selectedSSHKey = null; + // var sshkeyList = null; + $.fn.isVisible = function() { + return $.expr.filters.visible(this[0]); + } + function createSSHKeyList(options) { + options = options || {}; + var minHeight = "33px"; + var maxHeight = options.height || "120px"; + // var container = $('
    ',{style:"min-height: "+height+"; height: "+height+";"}); + var container = $('
    ',{style:"max-height: "+maxHeight+";"}); + + // var sshkeyList = $('
      ',{class:"projects-dialog-sshkey-list", style:"height:"+height}).appendTo(container).editableList({ + 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.isVisible() ) { + 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) { + console.log('key:', 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); + } + console.log('container.sshkeys:', container.data('sshkeys')); + return container; + } + function getSelectedSSHKey(container) { + var selected = $.data(container[0], 'selected'); + if ( container && selected ) { + return selected; + } + else { + return null; + } + } + function refreshSSHKeyList(container) { + console.log('refreshSSHKeyList'); + var sshkeyList = $.data(container[0], 'sshkeys'); + console.log(' ---> container:', container); + console.log(' ---> container.sshkeyList:', sshkeyList); + if ( container && sshkeyList ) { + sshkeyList.empty(); + $.getJSON("settings/user/keys", function(data) { + var keyList = $.data(container[0], 'sshkeys'); + data.keys.forEach(function(key) { + console.log('key:', 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); @@ -1013,7 +1141,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 410b3da63..cec421b13 100644 --- a/editor/sass/projects.scss +++ b/editor/sass/projects.scss @@ -654,6 +654,94 @@ float: right; } + + +.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; + } +} + /* .expandable-list-entry { .exandable-list-entry-header { diff --git a/red/runtime/storage/localfilesystem/projects/Project.js b/red/runtime/storage/localfilesystem/projects/Project.js index 67aaaadf9..58d3bb786 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"); @@ -680,6 +681,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, @@ -688,6 +690,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 === '_') ? os.hostname() + '_' + 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 2152390e4..374df51ae 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"); @@ -41,6 +42,11 @@ function runGitCommand(args,cwd,env) { }); child.on('close', function(code) { + console.log("==============================================================="); + console.log("stdout:", stdout); + console.log("==============================================================="); + console.log("stderr:", stderr); + console.log("==============================================================="); if (code !== 0) { var err = new Error(stderr); err.stdout = stdout; @@ -49,6 +55,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)) { @@ -56,9 +64,10 @@ function runGitCommand(args,cwd,env) { } else if (/CONFLICT/.test(err.stdout)) { err.code = "git_pull_merge_conflict"; } - - - + console.log("==============================================================="); + console.log('err:', err); + console.log("==============================================================="); + return reject(err); } resolve(stdout); @@ -78,6 +87,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; @@ -331,7 +356,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) } @@ -362,7 +392,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) } @@ -387,7 +422,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); } @@ -442,7 +482,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/projects/index.js b/red/runtime/storage/localfilesystem/projects/index.js index 15003f4d5..20c8515ba 100644 --- a/red/runtime/storage/localfilesystem/projects/index.js +++ b/red/runtime/storage/localfilesystem/projects/index.js @@ -134,7 +134,7 @@ function getProject(user, name) { return Projects.get(name).then(function(project) { var result = project.toJSON(); var projectSettings = settings.get("projects").projects; - if (projectSettings[name].git && projectSettings[name].git.user[username]) { + if (projectSettings[name] && projectSettings[name].git && projectSettings[name].git.user[username]) { result.git.user = projectSettings[name].git.user[username]; } return result; From bf57cb209fa6826acd8b18c027d80496dd4072d5 Mon Sep 17 00:00:00 2001 From: Hideki Nakamura Date: Sat, 16 Dec 2017 00:46:05 +0900 Subject: [PATCH 05/12] Delete unnecessary logs --- red/api/editor/sshkeys.js | 6 +++--- red/runtime/storage/localfilesystem/projects/git/index.js | 5 ----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/red/api/editor/sshkeys.js b/red/api/editor/sshkeys.js index a7b3a9181..3318d6a98 100644 --- a/red/api/editor/sshkeys.js +++ b/red/api/editor/sshkeys.js @@ -60,7 +60,7 @@ module.exports = { // Get SSH key detail app.get("/:id", needsPermission("settings.read"), function(req,res) { var username = getUsername(req.user); - console.log('username:', username); + // console.log('username:', username); runtime.storage.sshkeys.getSSHKey(username, req.params.id) .then(function(data) { res.json({ @@ -80,11 +80,11 @@ module.exports = { // Generate a SSH key app.post("/", needsPermission("settings.write"), function(req,res) { var username = getUsername(req.user); - console.log('req.body:', req.body); + // console.log('req.body:', req.body); if ( req.body && req.body.name ) { runtime.storage.sshkeys.generateSSHKey(username, req.body.email || "", req.body.name, req.body) .then(function(name) { - console.log('generate key --- success name:', name); + // console.log('generate key --- success name:', name); res.json({ name: name }); diff --git a/red/runtime/storage/localfilesystem/projects/git/index.js b/red/runtime/storage/localfilesystem/projects/git/index.js index eda7b49e5..8420e223c 100644 --- a/red/runtime/storage/localfilesystem/projects/git/index.js +++ b/red/runtime/storage/localfilesystem/projects/git/index.js @@ -43,11 +43,6 @@ function runGitCommand(args,cwd,env) { }); child.on('close', function(code) { - console.log("==============================================================="); - console.log("stdout:", stdout); - console.log("==============================================================="); - console.log("stderr:", stderr); - console.log("==============================================================="); if (code !== 0) { var err = new Error(stderr); err.stdout = stdout; From 53e012f29648775c7d8a97f8fbc6ad4ff5ca80fa Mon Sep 17 00:00:00 2001 From: Hideki Nakamura Date: Mon, 18 Dec 2017 00:53:03 +0900 Subject: [PATCH 06/12] Add delete SSH Key dialog --- editor/js/ui/projectUserSettings.js | 60 +++++++++++++++++++++++++---- editor/js/ui/projects.js | 8 ---- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/editor/js/ui/projectUserSettings.js b/editor/js/ui/projectUserSettings.js index afb111134..3c32984a8 100644 --- a/editor/js/ui/projectUserSettings.js +++ b/editor/js/ui/projectUserSettings.js @@ -53,15 +53,55 @@ RED.projects.userSettings = (function() { var sshkeyListOptions = { height: "300px", deleteAction: function(entry, header) { - sendSSHKeyManagementAPI("DELETE_KEY", entry.name, function(data) { - hideSSHKeyGenerateForm(); - utils.refreshSSHKeyList(sshkeysList); + var spinner = utils.addSpinnerOverlay(header).addClass('projects-dialog-spinner-contain'); + var notification = RED.notify("Are you sure you want to delete the SSH Keys '"+entry.name+"'? This cannot be undone.", { + type: 'warning', + modal: true, + fixed: true, + buttons: [ + { + text: RED._("common.label.cancel"), + click: function() { + spinner.remove(); + notification.close(); + } + }, + { + text: "Delete SSH Keys", + click: function() { + notification.close(); + sendSSHKeyManagementAPI("DELETE_KEY", entry.name, null, function(data) { + spinner.remove(); + hideSSHKeyGenerateForm(); + utils.refreshSSHKeyList(sshkeysList); + }, function(err) { + spinner.remove(); + console.log('Delete error! error:', err); + notification = RED.notify("Failed to delete the SSH Keys '"+entry.name+"'.", { + type: "error", + modal: true, + fixed: false + }); + }); + } + } + ] }); }, selectAction: function(entry, header) { - sendSSHKeyManagementAPI("GET_KEY_DETAIL", entry.name, function(data) { + var spinner = utils.addSpinnerOverlay(header).addClass('projects-dialog-spinner-contain'); + sendSSHKeyManagementAPI("GET_KEY_DETAIL", entry.name, null, function(data) { + spinner.remove(); setDialogContext(entry.name, data.publickey); dialog.dialog("open"); + }, function(err) { + console.log('Get SSH Key detail error! error:', err); + spinner.remove(); + notification = RED.notify("Failed to get the SSH Key detail '"+entry.name+"'.", { + type: "error", + modal: true, + fixed: false + }); }); } }; @@ -134,7 +174,8 @@ RED.projects.userSettings = (function() { email: gitEmailInput.val(), password: sshkeyPassphraseInput.val(), size: 4096 - }, + }, + gitconfigContainer, function() { hideSSHKeyGenerateForm(); utils.refreshSSHKeyList(sshkeysList); @@ -154,7 +195,7 @@ RED.projects.userSettings = (function() { }); } - function sendSSHKeyManagementAPI(type, param, successCallback, failCallback) { + function sendSSHKeyManagementAPI(type, param, overlay, successCallback, failCallback) { var url; var method; var payload; @@ -180,10 +221,13 @@ RED.projects.userSettings = (function() { console.error('Unexpected type....'); return; } - var spinner = utils.addSpinnerOverlay(gitconfigContainer); + // var spinner = utils.addSpinnerOverlay(gitconfigContainer); + var spinner = overlay ? utils.addSpinnerOverlay(overlay) : null; var done = function(err) { - spinner.remove(); + if ( spinner ) { + spinner.remove(); + } if (err) { console.log(err); return; diff --git a/editor/js/ui/projects.js b/editor/js/ui/projects.js index 665444656..4ea645fb8 100644 --- a/editor/js/ui/projects.js +++ b/editor/js/ui/projects.js @@ -728,8 +728,6 @@ RED.projects = (function() { return container; } - // var selectedSSHKey = null; - // var sshkeyList = null; $.fn.isVisible = function() { return $.expr.filters.visible(this[0]); } @@ -774,7 +772,6 @@ RED.projects = (function() { }); $.getJSON("settings/user/keys", function(data) { data.keys.forEach(function(key) { - console.log('key:', key); if ( sshkeyList ) { sshkeyList.editableList('addItem',key); } @@ -788,7 +785,6 @@ RED.projects = (function() { $.data(container[0], 'selected', null); $.data(container[0], 'sshkeys', sshkeyList); } - console.log('container.sshkeys:', container.data('sshkeys')); return container; } function getSelectedSSHKey(container) { @@ -801,16 +797,12 @@ RED.projects = (function() { } } function refreshSSHKeyList(container) { - console.log('refreshSSHKeyList'); var sshkeyList = $.data(container[0], 'sshkeys'); - console.log(' ---> container:', container); - console.log(' ---> container.sshkeyList:', sshkeyList); if ( container && sshkeyList ) { sshkeyList.empty(); $.getJSON("settings/user/keys", function(data) { var keyList = $.data(container[0], 'sshkeys'); data.keys.forEach(function(key) { - console.log('key:', key); if ( keyList ) { keyList.editableList('addItem',key); } From d35784ec612f956814db72d8a751b8f28b68481b Mon Sep 17 00:00:00 2001 From: Hideki Nakamura Date: Mon, 18 Dec 2017 20:46:07 +0900 Subject: [PATCH 07/12] Delete unnecessary comment & Add a logic of the error case --- editor/js/ui/projects.js | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/editor/js/ui/projects.js b/editor/js/ui/projects.js index 4ea645fb8..5810cc1a0 100644 --- a/editor/js/ui/projects.js +++ b/editor/js/ui/projects.js @@ -165,7 +165,7 @@ RED.projects = (function() { } } } - + $("#projects-dialog-create").prop('disabled',!valid).toggleClass('disabled ui-button-disabled ui-state-disabled',!valid); } @@ -417,15 +417,22 @@ RED.projects = (function() { var repoUrl = projectRepoInput.val(); var metaData = {}; if (/^(?:ssh|[\d\w\.\-_]+@[\w\.]+):(?:\/\/)?/.test(repoUrl)) { - projectData.git = { - remotes: { - 'origin': { - url: repoUrl, - key_file: getSelectedSSHKey(projectRepoSSHKeySelect).name, - passphrase: projectRepoPassphrase.val() + 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 = { @@ -735,10 +742,8 @@ RED.projects = (function() { options = options || {}; var minHeight = "33px"; var maxHeight = options.height || "120px"; - // var container = $('
        ',{style:"min-height: "+height+"; height: "+height+";"}); var container = $('
        ',{style:"max-height: "+maxHeight+";"}); - // var sshkeyList = $('
          ',{class:"projects-dialog-sshkey-list", style:"height:"+height}).appendTo(container).editableList({ var sshkeyList = $('
            ',{class:"projects-dialog-sshkey-list", style:"max-height:"+maxHeight+";min-height:"+minHeight+";"}).appendTo(container).editableList({ addButton: false, scrollOnAdd: false, From c09a407f4c6d85e5335e2468e25454d235ce8029 Mon Sep 17 00:00:00 2001 From: Hideki Nakamura Date: Mon, 18 Dec 2017 21:37:09 +0900 Subject: [PATCH 08/12] Delete unnecessary comments & add trace mock function --- test/red/api/editor/sshkeys_spec.js | 24 +------------------ .../storage/localfilesystem/sshkeys_spec.js | 3 ++- 2 files changed, 3 insertions(+), 24 deletions(-) diff --git a/test/red/api/editor/sshkeys_spec.js b/test/red/api/editor/sshkeys_spec.js index 31ed6f865..ade1e7a07 100644 --- a/test/red/api/editor/sshkeys_spec.js +++ b/test/red/api/editor/sshkeys_spec.js @@ -37,29 +37,13 @@ describe("api/editor/sshkeys", function() { var isStarted = true; var errors = []; var session_data = {}; - // before(function() { - // mockList.forEach(function(m) { - // sinon.stub(require("../../../../../red/api/editor/"+m),"init",function(){}); - // }); - // sinon.stub(require("../../../../../red/api/editor/theme"),"app",function(){ return express()}); - // }); - // after(function() { - // mockList.forEach(function(m) { - // require("../../../../../red/api/editor/"+m).init.restore(); - // }) - // require("../../../../../red/api/editor/theme").app.restore(); - // }); + var mockRuntime = { settings:{ httpNodeRoot: true, httpAdminRoot: true, disableEditor: false, exportNodeSettings:function(){}, - // adminAuth: { - // default: { - // permissions: ['*'] - // } - // }, storage: { getSessions: function(){ return when.resolve(session_data); @@ -92,7 +76,6 @@ describe("api/editor/sshkeys", function() { app.use(editorApi.init({},mockRuntime)); }); after(function() { - // fs.removeSync() }) beforeEach(function() { @@ -170,7 +153,6 @@ describe("api/editor/sshkeys", function() { it('GET /settings/user/keys --- return Unexpected 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") @@ -227,7 +209,6 @@ describe("api/editor/sshkeys", function() { it('GET /settings/user/keys/ --- return Unexpected 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) @@ -264,7 +245,6 @@ describe("api/editor/sshkeys", function() { mockRuntime.storage.sshkeys.generateSSHKey.returns(Promise.resolve(key_file_name)); request(app) .post("/settings/user/keys") - // .send({ name: key_file_name }) .expect(400) .end(function(err,res) { if (err) { @@ -302,7 +282,6 @@ describe("api/editor/sshkeys", function() { it('POST /settings/user/keys --- return Unexpected 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") @@ -358,7 +337,6 @@ describe("api/editor/sshkeys", function() { it('DELETE /settings/user/keys/ --- return Unexpected 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) diff --git a/test/red/runtime/storage/localfilesystem/sshkeys_spec.js b/test/red/runtime/storage/localfilesystem/sshkeys_spec.js index 8f6619414..a705e703b 100644 --- a/test/red/runtime/storage/localfilesystem/sshkeys_spec.js +++ b/test/red/runtime/storage/localfilesystem/sshkeys_spec.js @@ -28,7 +28,8 @@ describe("storage/localfilesystem/sshkeys", function() { var mockRuntime = { log:{ _:function() { return "placeholder message"}, - info: function() { } + info: function() { }, + trace: function() { } } }; beforeEach(function(done) { From ed52e5afd17df2b484e33255e91612f4b2c5c65b Mon Sep 17 00:00:00 2001 From: Hideki Nakamura Date: Tue, 19 Dec 2017 10:32:05 +0900 Subject: [PATCH 09/12] Avoid reformatting package.json --- package.json | 99 ++++++++++++++++++++++++---------------------------- 1 file changed, 46 insertions(+), 53 deletions(-) diff --git a/package.json b/package.json index 4e18bad23..3d9dbabcd 100644 --- a/package.json +++ b/package.json @@ -1,100 +1,93 @@ { - "name": "node-red", - "version": "0.17.5", - "description": "A visual tool for wiring the Internet of Things", - "homepage": "http://nodered.org", - "license": "Apache-2.0", - "repository": { - "type": "git", - "url": "https://github.com/node-red/node-red.git" + "name" : "node-red", + "version" : "0.17.5", + "description" : "A visual tool for wiring the Internet of Things", + "homepage" : "http://nodered.org", + "license" : "Apache-2.0", + "repository" : { + "type":"git", + "url":"https://github.com/node-red/node-red.git" }, - "main": "red/red.js", - "scripts": { + "main" : "red/red.js", + "scripts" : { "start": "node red.js", "test": "grunt", "build": "grunt build" }, - "bin": { + "bin" : { "node-red": "./red.js", "node-red-pi": "bin/node-red-pi" }, "contributors": [ - { - "name": "Nick O'Leary" - }, - { - "name": "Dave Conway-Jones" - } + {"name": "Nick O'Leary"}, + {"name": "Dave Conway-Jones"} ], "keywords": [ - "editor", - "messaging", - "iot", - "flow" + "editor", "messaging", "iot", "flow" ], "dependencies": { "basic-auth": "1.1.0", "bcryptjs": "2.4.3", "body-parser": "1.17.2", - "cheerio": "0.22.0", + "cheerio":"0.22.0", "clone": "2.1.1", "cookie": "0.3.1", "cookie-parser": "1.4.3", - "cors": "2.8.3", - "cron": "1.2.1", + "cors":"2.8.3", + "cron":"1.2.1", "express": "4.15.3", "express-session": "1.15.2", - "follow-redirects": "1.2.4", + "follow-redirects":"1.2.4", "fs-extra": "4.0.2", - "fs.notify": "0.0.4", - "hash-sum": "1.0.2", - "i18next": "1.10.6", - "is-utf8": "0.2.1", + "fs.notify":"0.0.4", + "hash-sum":"1.0.2", + "i18next":"1.10.6", + "is-utf8":"0.2.1", "js-yaml": "3.8.4", - "json-stringify-safe": "5.0.1", - "jsonata": "1.2.6", + "json-stringify-safe":"5.0.1", + "jsonata":"1.2.6", "media-typer": "0.3.0", "mqtt": "2.9.0", "multer": "1.3.0", "mustache": "2.3.0", - "node-red-node-email": "0.1.*", - "node-red-node-feedparser": "0.1.*", - "node-red-node-rbe": "0.1.*", - "node-red-node-twitter": "0.1.*", "nopt": "3.0.6", - "oauth2orize": "1.8.0", - "on-headers": "1.0.1", - "passport": "0.3.2", - "passport-http-bearer": "1.0.1", - "passport-oauth2-client-password": "0.1.2", - "raw-body": "2.2.0", + "oauth2orize":"1.8.0", + "on-headers":"1.0.1", + "passport":"0.3.2", + "passport-http-bearer":"1.0.1", + "passport-oauth2-client-password":"0.1.2", + "raw-body":"2.2.0", "semver": "5.3.0", - "sentiment": "2.1.0", - "ssh-keygen": "^0.4.1", - "uglify-js": "3.0.20", + "sentiment":"2.1.0", + "ssh-keygen":"0.4.1", + "uglify-js":"3.0.20", "when": "3.7.8", "ws": "1.1.1", - "xml2js": "0.4.17" + "xml2js":"0.4.17", + "node-red-node-feedparser":"0.1.*", + "node-red-node-email":"0.1.*", + "node-red-node-twitter":"0.1.*", + "node-red-node-rbe":"0.1.*" }, "optionalDependencies": { - "bcrypt": "~1.0.1" + "bcrypt":"~1.0.1" }, "devDependencies": { "grunt": "~1.0.1", "grunt-chmod": "~1.1.1", "grunt-cli": "~1.2.0", - "grunt-concurrent": "~2.3.1", - "grunt-contrib-clean": "~1.1.0", + "grunt-concurrent":"~2.3.1", + "grunt-contrib-clean":"~1.1.0", "grunt-contrib-compress": "~1.4.0", - "grunt-contrib-concat": "~1.0.1", + "grunt-contrib-concat":"~1.0.1", "grunt-contrib-copy": "~1.0.0", "grunt-contrib-jshint": "~1.1.0", "grunt-contrib-uglify": "~3.0.1", - "grunt-contrib-watch": "~1.0.0", - "grunt-jsonlint": "~1.1.0", + "grunt-contrib-watch":"~1.0.0", + "grunt-jsonlint":"~1.1.0", "grunt-mocha-istanbul": "5.0.2", - "grunt-nodemon": "~0.4.2", - "grunt-sass": "~1.2.1", + "grunt-nodemon":"~0.4.2", + "grunt-sass":"~1.2.1", "grunt-simple-mocha": "~0.4.1", "istanbul": "0.4.5", "mocha": "~3.4.2", From e07a4dc7bac6ce378af77e4945d857d240427430 Mon Sep 17 00:00:00 2001 From: Hideki Nakamura Date: Tue, 19 Dec 2017 22:30:42 +0900 Subject: [PATCH 10/12] Change the implementation of visible check --- editor/js/ui/projects.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/editor/js/ui/projects.js b/editor/js/ui/projects.js index 5810cc1a0..aa4ade0e3 100644 --- a/editor/js/ui/projects.js +++ b/editor/js/ui/projects.js @@ -735,9 +735,6 @@ RED.projects = (function() { return container; } - $.fn.isVisible = function() { - return $.expr.filters.visible(this[0]); - } function createSSHKeyList(options) { options = options || {}; var minHeight = "33px"; @@ -765,7 +762,7 @@ RED.projects = (function() { $('',{class:"fa fa-trash-o"}).appendTo(deleteButton); header.addClass("selectable"); row.click(function(evt) { - if ( !deleteButton.isVisible() ) { + if ( !deleteButton.is(":visible") ) { if ( options.selectAction ) { options.selectAction(entry, header); } From c24b0c6bb4849f493160153c75ba0998c4c89d99 Mon Sep 17 00:00:00 2001 From: Hideki Nakamura Date: Tue, 19 Dec 2017 22:57:40 +0900 Subject: [PATCH 11/12] Change "generateSSHKey" function signature --- red/api/editor/sshkeys.js | 2 +- .../storage/localfilesystem/sshkeys.js | 9 ++- .../storage/localfilesystem/sshkeys_spec.js | 71 ++++++++++--------- 3 files changed, 44 insertions(+), 38 deletions(-) diff --git a/red/api/editor/sshkeys.js b/red/api/editor/sshkeys.js index 3318d6a98..d1338e19f 100644 --- a/red/api/editor/sshkeys.js +++ b/red/api/editor/sshkeys.js @@ -82,7 +82,7 @@ module.exports = { var username = getUsername(req.user); // console.log('req.body:', req.body); if ( req.body && req.body.name ) { - runtime.storage.sshkeys.generateSSHKey(username, req.body.email || "", req.body.name, req.body) + runtime.storage.sshkeys.generateSSHKey(username, req.body) .then(function(name) { // console.log('generate key --- success name:', name); res.json({ diff --git a/red/runtime/storage/localfilesystem/sshkeys.js b/red/runtime/storage/localfilesystem/sshkeys.js index 96097aeba..fce84b469 100644 --- a/red/runtime/storage/localfilesystem/sshkeys.js +++ b/red/runtime/storage/localfilesystem/sshkeys.js @@ -83,16 +83,21 @@ function getSSHKey(username, name) { }); } -function generateSSHKey(username, email, name, data) { +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, data.password, data.size) + return generateSSHKeyPair(privateKeyFilePath, email, password, size) .then(function() { return name; }); diff --git a/test/red/runtime/storage/localfilesystem/sshkeys_spec.js b/test/red/runtime/storage/localfilesystem/sshkeys_spec.js index a705e703b..370ba9908 100644 --- a/test/red/runtime/storage/localfilesystem/sshkeys_spec.js +++ b/test/red/runtime/storage/localfilesystem/sshkeys_spec.js @@ -245,15 +245,16 @@ describe("storage/localfilesystem/sshkeys", function() { it('should generate sshkey file with empty data', function(done) { var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys'); var username = 'test'; - var email = 'test@test.com'; - var filename = 'test-key01'; - var data = {}; + var options = { + email: 'test@test.com', + name: 'test-key01' + }; localfilesystem.init(mockSettings, mockRuntime).then(function() { sshkeys.init(mockSettings, mockRuntime).then(function() { - sshkeys.generateSSHKey(username, email, filename, data).then(function(retObj) { - retObj.should.be.equal(filename); - fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename)).should.be.true(); - fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename+'.pub')).should.be.true(); + 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); @@ -269,17 +270,17 @@ describe("storage/localfilesystem/sshkeys", function() { it('should generate sshkey file with password data', function(done) { var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys'); var username = 'test'; - var email = 'test@test.com'; - var filename = 'test-key01'; - var data = { + 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, email, filename, data).then(function(retObj) { - retObj.should.be.equal(filename); - fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename)).should.be.true(); - fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename+'.pub')).should.be.true(); + 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); @@ -295,17 +296,17 @@ describe("storage/localfilesystem/sshkeys", function() { it('should generate sshkey file with size data', function(done) { var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys'); var username = 'test'; - var email = 'test@test.com'; - var filename = 'test-key01'; - var data = { + 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, email, filename, data).then(function(retObj) { - retObj.should.be.equal(filename); - fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename)).should.be.true(); - fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename+'.pub')).should.be.true(); + 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); @@ -322,18 +323,18 @@ describe("storage/localfilesystem/sshkeys", function() { this.timeout(5000); var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys'); var username = 'test'; - var email = 'test@test.com'; - var filename = 'test-key01'; - var data = { + 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, email, filename, data).then(function(retObj) { - retObj.should.be.equal(filename); - fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename)).should.be.true(); - fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename+'.pub')).should.be.true(); + 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); @@ -350,17 +351,17 @@ describe("storage/localfilesystem/sshkeys", function() { this.timeout(5000); var sshkeyDirPath = path.join(userDir, 'projects', '.sshkeys'); var username = 'test'; - var email = 'test@test.com'; - var filename = 'test-key01'; - var data = { + 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, email, filename, data).then(function(retObj) { - retObj.should.be.equal(filename); - fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename)).should.be.true(); - fs.existsSync(path.join(sshkeyDirPath,username+'_'+filename+'.pub')).should.be.true(); + 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); From 2ea2af7d2a74ac971428a8d2394509240f9ea100 Mon Sep 17 00:00:00 2001 From: Hideki Nakamura Date: Wed, 20 Dec 2017 19:44:57 +0900 Subject: [PATCH 12/12] Use a fixed string instead of `os.hostname()` --- red/api/editor/sshkeys.js | 2 +- red/runtime/storage/localfilesystem/projects/Project.js | 2 +- red/runtime/storage/localfilesystem/sshkeys.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/red/api/editor/sshkeys.js b/red/api/editor/sshkeys.js index d1338e19f..08ae241ed 100644 --- a/red/api/editor/sshkeys.js +++ b/red/api/editor/sshkeys.js @@ -21,7 +21,7 @@ var settings; var needsPermission = require("../auth").needsPermission; function getUsername(userObj) { - var username = os.hostname(); + var username = '.default'; if ( userObj && userObj.name ) { username = userObj.name; } diff --git a/red/runtime/storage/localfilesystem/projects/Project.js b/red/runtime/storage/localfilesystem/projects/Project.js index 632390e2b..d2007fbf9 100644 --- a/red/runtime/storage/localfilesystem/projects/Project.js +++ b/red/runtime/storage/localfilesystem/projects/Project.js @@ -704,7 +704,7 @@ 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 === '_') ? os.hostname() + '_' + originRemote.key_file : username + '_' + originRemote.key_file; + 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 diff --git a/red/runtime/storage/localfilesystem/sshkeys.js b/red/runtime/storage/localfilesystem/sshkeys.js index fce84b469..4a5211cde 100644 --- a/red/runtime/storage/localfilesystem/sshkeys.js +++ b/red/runtime/storage/localfilesystem/sshkeys.js @@ -44,7 +44,7 @@ function listSSHKeys(username) { var ret = fns.sort() .filter(function(fn) { var fullPath = fspath.join(sshkeyDir,fn); - if (fn[0] != ".") { + if (fn.length > 2 || fn[0] != ".") { var stats = fs.lstatSync(fullPath); if (stats.isFile()) { return fn.startsWith(startStr);