diff --git a/src/driver.js b/src/driver.js index 16753b30b..657b8653c 100644 --- a/src/driver.js +++ b/src/driver.js @@ -31,6 +31,7 @@ import { import Session from './session' import RxSession from './session-rx' import { ALL } from './internal/request-message' +import { ENCRYPTION_ON, ENCRYPTION_OFF } from './internal/util' const DEFAULT_MAX_CONNECTION_LIFETIME = 60 * 60 * 1000 // 1 hour @@ -136,6 +137,34 @@ class Driver { return connectionProvider.supportsTransactionConfig() } + /** + * @protected + * @returns {boolean} + */ + _supportsRouting () { + return false + } + + /** + * Returns boolean to indicate if driver has been configured with encryption enabled. + * + * @protected + * @returns {boolean} + */ + _isEncrypted () { + return this._config.encrypted === ENCRYPTION_ON + } + + /** + * Returns the configured trust strategy that the driver has been configured with. + * + * @protected + * @returns {TrustStrategy} + */ + _getTrust () { + return this._config.trust + } + /** * Acquire a session to communicate with the database. The session will * borrow connections from the underlying connection pool as required and diff --git a/src/index.js b/src/index.js index 0800bac53..2febf75f2 100644 --- a/src/index.js +++ b/src/index.js @@ -37,7 +37,12 @@ import Record from './record' import { Driver, READ, WRITE } from './driver' import RoutingDriver from './routing-driver' import VERSION from './version' -import { assertString, isEmptyObjectOrNull } from './internal/util' +import { + ENCRYPTION_ON, + ENCRYPTION_OFF, + assertString, + isEmptyObjectOrNull +} from './internal/util' import urlUtil from './internal/url-util' import { isPoint, Point } from './spatial-types' import { @@ -178,7 +183,52 @@ import ServerAddress from './internal/server-address' function driver (url, authToken, config = {}) { assertString(url, 'Bolt URL') const parsedUrl = urlUtil.parseDatabaseUrl(url) - if (parsedUrl.scheme === 'neo4j') { + + // Determine entryption/trust options from the URL. + let routing = false + let encrypted = false + let trust + switch (parsedUrl.scheme) { + case 'bolt': + break + case 'bolt+s': + encrypted = true + trust = 'TRUST_SYSTEM_CA_SIGNED_CERTIFICATES' + break + case 'bolt+ssc': + encrypted = true + trust = 'TRUST_ALL_CERTIFICATES' + break + case 'neo4j': + routing = true + break + case 'neo4j+s': + encrypted = true + trust = 'TRUST_SYSTEM_CA_SIGNED_CERTIFICATES' + routing = true + break + case 'neo4j+ssc': + encrypted = true + trust = 'TRUST_ALL_CERTIFICATES' + routing = true + break + default: + throw new Error(`Unknown scheme: ${parsedUrl.scheme}`) + } + + // Encryption enabled on URL, propagate trust to the config. + if (encrypted) { + // Check for configuration conflict between URL and config. + if ('encrypted' in config || 'trust' in config) { + throw new Error( + 'Encryption/trust can only be configured either through URL or config, not both' + ) + } + config.encrypted = ENCRYPTION_ON + config.trust = trust + } + + if (routing) { return new RoutingDriver( ServerAddress.fromUrl(parsedUrl.hostAndPort), parsedUrl.query, @@ -186,10 +236,10 @@ function driver (url, authToken, config = {}) { authToken, config ) - } else if (parsedUrl.scheme === 'bolt') { + } else { if (!isEmptyObjectOrNull(parsedUrl.query)) { throw new Error( - `Parameters are not supported with scheme 'bolt'. Given URL: '${url}'` + `Parameters are not supported with none routed scheme. Given URL: '${url}'` ) } return new Driver( @@ -198,8 +248,6 @@ function driver (url, authToken, config = {}) { authToken, config ) - } else { - throw new Error(`Unknown scheme: ${parsedUrl.scheme}`) } } diff --git a/src/routing-driver.js b/src/routing-driver.js index f7b022450..2c698afd8 100644 --- a/src/routing-driver.js +++ b/src/routing-driver.js @@ -52,6 +52,10 @@ class RoutingDriver extends Driver { authToken: authToken }) } + + _supportsRouting () { + return true + } } /** diff --git a/test/driver.test.js b/test/driver.test.js index 2140bbe25..c09574bdf 100644 --- a/test/driver.test.js +++ b/test/driver.test.js @@ -27,6 +27,72 @@ import { import { ServerVersion, VERSION_4_0_0 } from '../src/internal/server-version' import testUtils from './internal/test-utils' +// As long as driver creation doesn't touch the network it's fine to run +// this as a unit test. +describe('#unit driver', () => { + let driver + + afterEach(async () => { + if (driver) { + await driver.close() + } + }) + + it('should create an unencrypted, non-routed driver for scheme: bolt', () => { + driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken) + expect(driver._isEncrypted()).toBeFalsy() + expect(driver._supportsRouting()).toBeFalsy() + }) + + it('should create an encrypted, system CAs trusting, non-routed driver for scheme: bolt+s', () => { + driver = neo4j.driver('bolt+s://localhost', sharedNeo4j.authToken) + expect(driver._isEncrypted()).toBeTruthy() + expect(driver._getTrust()).toEqual('TRUST_SYSTEM_CA_SIGNED_CERTIFICATES') + expect(driver._supportsRouting()).toBeFalsy() + }) + + it('should create an encrypted, all trusting, non-routed driver for scheme: bolt+ssc', () => { + driver = neo4j.driver('bolt+ssc://localhost', sharedNeo4j.authToken) + expect(driver._isEncrypted()).toBeTruthy() + expect(driver._getTrust()).toEqual('TRUST_ALL_CERTIFICATES') + expect(driver._supportsRouting()).toBeFalsy() + }) + + it('should create an unencrypted, routed driver for scheme: neo4j', () => { + driver = neo4j.driver('neo4j://localhost', sharedNeo4j.authToken) + expect(driver._isEncrypted()).toBeFalsy() + expect(driver._supportsRouting()).toBeTruthy() + }) + + it('should create an encrypted, system CAs trusting, routed driver for scheme: neo4j+s', () => { + driver = neo4j.driver('neo4j+s://localhost', sharedNeo4j.authToken) + expect(driver._isEncrypted()).toBeTruthy() + expect(driver._getTrust()).toEqual('TRUST_SYSTEM_CA_SIGNED_CERTIFICATES') + expect(driver._supportsRouting()).toBeTruthy() + }) + + it('should create an encrypted, all trusting, routed driver for scheme: neo4j+ssc', () => { + driver = neo4j.driver('neo4j+ssc://localhost', sharedNeo4j.authToken) + expect(driver._isEncrypted()).toBeTruthy() + expect(driver._getTrust()).toEqual('TRUST_ALL_CERTIFICATES') + expect(driver._supportsRouting()).toBeTruthy() + }) + + it('should throw when encryption in url AND in config', () => { + expect(() => + neo4j.driver('neo4j+ssc://localhost', sharedNeo4j.authToken, { + encrypted: 'ENCRYPTION_OFF' + }) + ).toThrow() + // Throw even in case where there is no conflict + expect(() => + neo4j.driver('neo4j+s://localhost', sharedNeo4j.authToken, { + encrypted: 'ENCRYPTION_ON' + }) + ).toThrow() + }) +}) + describe('#integration driver', () => { let driver let serverVersion diff --git a/test/internal/node/tls.test.js b/test/internal/node/tls.test.js index b520d9309..b263b8719 100644 --- a/test/internal/node/tls.test.js +++ b/test/internal/node/tls.test.js @@ -57,6 +57,20 @@ describe('#integration trust', () => { done() }) }) + + it('should work with default certificate using URL scheme', done => { + // Given + driver = neo4j.driver('bolt+ssc://localhost', sharedNeo4j.authToken) + + // When + driver + .session() + .run('RETURN 1') + .then(result => { + expect(result.records[0].get(0).toNumber()).toBe(1) + done() + }) + }) }) describe('trust-custom-ca-signed-certificates', () => { @@ -128,6 +142,20 @@ describe('#integration trust', () => { }) }) + it('should reject unknown certificates using URL scheme', done => { + // Given + driver = neo4j.driver('bolt+s://localhost', sharedNeo4j.authToken) + + // When + driver + .session() + .run('RETURN 1') + .catch(err => { + expect(err.message).toContain('Server certificate is not trusted') + done() + }) + }) + it('should reject unknown certificates if trust not specified', done => { // Given driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken, {