diff --git a/src/v1/internal/ch-node.js b/src/v1/internal/ch-node.js index ed932f478..4ff6e0d29 100644 --- a/src/v1/internal/ch-node.js +++ b/src/v1/internal/ch-node.js @@ -45,7 +45,7 @@ function loadFingerprint( serverId, knownHostsPath, cb ) { require('readline').createInterface({ input: fs.createReadStream(knownHostsPath) }).on('line', (line) => { - if( line.startsWith( serverId )) { + if( !found && line.startsWith( serverId )) { found = true; cb( line.split(" ")[1] ); } @@ -56,8 +56,26 @@ function loadFingerprint( serverId, knownHostsPath, cb ) { }); } +const _lockFingerprintFromAppending = {}; function storeFingerprint( serverId, knownHostsPath, fingerprint ) { + // we check if the serverId has been appended + if(!!_lockFingerprintFromAppending[serverId]){ + // if it has, we ignore it + return; + } + + // we make the line as appended + // ( 1 is more efficient to store than true because true is an oddball ) + _lockFingerprintFromAppending[serverId] = 1; + + // we append to file fs.appendFile(knownHostsPath, serverId + " " + fingerprint + EOL, "utf8" ); + + // since the error occurs in the span of one tick + // after one tick we clean up to not interfere with anything else + setImmediate(() => { + delete _lockFingerprintFromAppending[serverId]; + }); } const TrustStrategy = { diff --git a/test/internal/tls.test.js b/test/internal/tls.test.js index ccd61bd9e..5112e2ad8 100644 --- a/test/internal/tls.test.js +++ b/test/internal/tls.test.js @@ -76,6 +76,41 @@ describe('trust-on-first-use', function() { var driver; + it('should not throw an error if the host file contains two host duplicates', function(done) { + 'use strict'; + // Assuming we only run this test on NodeJS with TOFU support + if( !hasFeature("trust_on_first_use") ) { + done(); + return; + } + + // Given + var knownHostsPath = "build/known_hosts"; + if( fs.existsSync(knownHostsPath) ) { + fs.unlinkSync(knownHostsPath); + } + + fs.writeFileSync( + knownHostsPath, + 'localhost:7687 abcd\n' + + 'localhost:7687 abcd' + ); + + driver = neo4j.driver("bolt://localhost", neo4j.auth.basic("neo4j", "neo4j"), { + encrypted: true, + trust: "TRUST_ON_FIRST_USE", + knownHosts: knownHostsPath + }); + + // When + driver.session().run("RETURN true AS a").then( function(data) { + // Then we get to here. + // And then the known_hosts file should have correct contents + expect( data.records[0].get('a') ).toBe( true ); + done(); + }); + }); + it('should accept previously un-seen hosts', function(done) { // Assuming we only run this test on NodeJS with TOFU support if( !hasFeature("trust_on_first_use") ) { @@ -104,6 +139,36 @@ describe('trust-on-first-use', function() { }); }); + it('should not duplicate fingerprint entries', function(done) { + // Assuming we only run this test on NodeJS with TOFU support + if( !hasFeature("trust_on_first_use") ) { + done(); + return; + } + + // Given + var knownHostsPath = "build/known_hosts"; + if( fs.existsSync(knownHostsPath) ) { + fs.unlinkSync(knownHostsPath); + } + fs.writeFileSync(knownHostsPath, ''); + + driver = neo4j.driver("bolt://localhost", neo4j.auth.basic("neo4j", "neo4j"), { + encrypted: true, + trust: "TRUST_ON_FIRST_USE", + knownHosts: knownHostsPath + }); + + // When + driver.session(); + driver.session(); + + setTimeout(function() { + expect( fs.readFileSync(knownHostsPath, 'utf8').split('\n').length ).toBe( 1 ); + done(); + }, 1000); + }); + it('should should give helpful error if database cert does not match stored certificate', function(done) { // Assuming we only run this test on NodeJS with TOFU support if( !hasFeature("trust_on_first_use") ) {