Skip to content

Commit 1418f42

Browse files
bigmontzrobsdedude
andauthored
Add acceptance test to neo4j-deno-driver and implement deno specific channel (#900)
Enabling the support for the acceptance tests to the generated DenoJS driver is a step towards to release it as library. During the acceptance tests run, the `browser-channel` presents some errors handling write request when the socket was already closed. Verifying the socket status does not prevent the error, since the sockets which appears as connected could also presents the error because of a non-updated status (or the status get changed while the message is being send). This error is no catchable and make the application get closed. `DenoChannel` was introduced for fixing this issue. This implementation uses the `Deno.Conn` and `Deno.TlsConn` for socket communication which causes a few drawbacks such as: * `TRUST_ALL_CERTIFICATES` should be handle by `--unsafely-ignore-certificate-errors` and not by driver configuration. See, https://deno.com/blog/v1.13#disable-tls-verification; * The `Deno.TlsConn` fails hard in certificate error which causes the application closes; ## Testkit Backend The Deno Testkit Backend implementation re-uses most of the NodeJS implementation. Adjustments in the file import and the removal of a direct `neo4j` dependency<sup>1</sup> in the common code was need for making it possible. Socket connection with testkit, backend controller and new entry-point were re-implemented for Deno backend. This implementation is located at `/packages/testkit-backend/deno` and documented at `/packages/testkit-backend/deno/README.md` <sup>1</sup> Other NodeJs/NPM dependencies like `tls` were also moved for Node specific files. Co-authored-by: Robsdedude <dev@rouvenbauer.de>
1 parent b8129ab commit 1418f42

39 files changed

+1164
-350
lines changed

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
3030
"packages/neo4j-driver/**/*.ts": [
3131
"npm run ts-standard::neo4j-driver",
3232
"git add"
33+
],
34+
"packages/testkit-backend/deno/**/*.{ts,js}": [
35+
"deno fmt",
36+
"deno lint",
37+
"git add"
3338
]
3439
},
3540
"scripts": {
@@ -46,6 +51,7 @@
4651
"start-neo4j": "lerna run start-neo4j --scope neo4j-driver",
4752
"stop-neo4j": "lerna run stop-neo4j --scope neo4j-driver",
4853
"start-testkit-backend": "lerna run start --scope testkit-backend --stream",
54+
"start-testkit-backend::deno": "lerna run start::deno --scope testkit-backend --stream",
4955
"lerna": "lerna",
5056
"prepare": "husky install",
5157
"lint-staged": "lint-staged",
Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
/**
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
/* eslint-disable */
20+
import ChannelBuffer from '../channel-buf'
21+
import { newError, internal } from 'neo4j-driver-core'
22+
import { iterateReader } from 'https://deno.land/std@0.157.0/streams/conversion.ts';
23+
24+
const {
25+
util: { ENCRYPTION_OFF, ENCRYPTION_ON }
26+
} = internal
27+
28+
let _CONNECTION_IDGEN = 0
29+
/**
30+
* Create a new DenoChannel to be used in Deno runtime.
31+
* @access private
32+
*/
33+
export default class DenoChannel {
34+
/**
35+
* Create new instance
36+
* @param {ChannelConfig} config - configuration for this channel.
37+
*/
38+
constructor (
39+
config,
40+
connect = _connect
41+
) {
42+
this.id = _CONNECTION_IDGEN++
43+
this._conn = null
44+
this._pending = []
45+
this._open = true
46+
this._error = null
47+
this._handleConnectionError = this._handleConnectionError.bind(this)
48+
this._handleConnectionTerminated = this._handleConnectionTerminated.bind(
49+
this
50+
)
51+
this._connectionErrorCode = config.connectionErrorCode
52+
this._receiveTimeout = null
53+
this._receiveTimeoutStarted = false
54+
this._receiveTimeoutId = null
55+
56+
this._config = config
57+
58+
connect(config)
59+
.then(conn => {
60+
this._clearConnectionTimeout()
61+
if (!this._open) {
62+
return conn.close()
63+
}
64+
this._conn = conn
65+
66+
setupReader(this)
67+
.catch(this._handleConnectionError)
68+
69+
const pending = this._pending
70+
this._pending = null
71+
for (let i = 0; i < pending.length; i++) {
72+
this.write(pending[i])
73+
}
74+
})
75+
.catch(this._handleConnectionError)
76+
77+
this._connectionTimeoutFired = false
78+
this._connectionTimeoutId = this._setupConnectionTimeout()
79+
}
80+
81+
_setupConnectionTimeout () {
82+
const timeout = this._config.connectionTimeout
83+
if (timeout) {
84+
return setTimeout(() => {
85+
this._connectionTimeoutFired = true
86+
this.close()
87+
.then(e => this._handleConnectionError(newError(`Connection timeout after ${timeout} ms`)))
88+
.catch(this._handleConnectionError)
89+
}, timeout)
90+
}
91+
return null
92+
}
93+
94+
/**
95+
* Remove active connection timeout, if any.
96+
* @private
97+
*/
98+
_clearConnectionTimeout () {
99+
const timeoutId = this._connectionTimeoutId
100+
if (timeoutId !== null) {
101+
this._connectionTimeoutFired = false
102+
this._connectionTimeoutId = null
103+
clearTimeout(timeoutId)
104+
}
105+
}
106+
107+
_handleConnectionError (err) {
108+
let msg =
109+
'Failed to connect to server. ' +
110+
'Please ensure that your database is listening on the correct host and port ' +
111+
'and that you have compatible encryption settings both on Neo4j server and driver. ' +
112+
'Note that the default encryption setting has changed in Neo4j 4.0.'
113+
if (err.message) msg += ' Caused by: ' + err.message
114+
this._error = newError(msg, this._connectionErrorCode)
115+
if (this.onerror) {
116+
this.onerror(this._error)
117+
}
118+
}
119+
120+
_handleConnectionTerminated () {
121+
this._open = false
122+
this._error = newError(
123+
'Connection was closed by server',
124+
this._connectionErrorCode
125+
)
126+
if (this.onerror) {
127+
this.onerror(this._error)
128+
}
129+
}
130+
131+
132+
/**
133+
* Write the passed in buffer to connection
134+
* @param {ChannelBuffer} buffer - Buffer to write
135+
*/
136+
write (buffer) {
137+
if (this._pending !== null) {
138+
this._pending.push(buffer)
139+
} else if (buffer instanceof ChannelBuffer) {
140+
this._conn.write(buffer._buffer).catch(this._handleConnectionError)
141+
} else {
142+
throw newError("Don't know how to send buffer: " + buffer)
143+
}
144+
}
145+
146+
/**
147+
* Close the connection
148+
* @returns {Promise} A promise that will be resolved after channel is closed
149+
*/
150+
async close () {
151+
if (this._open) {
152+
this._open = false
153+
if (this._conn != null) {
154+
await this._conn.close()
155+
}
156+
}
157+
}
158+
159+
/**
160+
* Setup the receive timeout for the channel.
161+
*
162+
* Not supported for the browser channel.
163+
*
164+
* @param {number} receiveTimeout The amount of time the channel will keep without receive any data before timeout (ms)
165+
* @returns {void}
166+
*/
167+
setupReceiveTimeout (receiveTimeout) {
168+
this._receiveTimeout = receiveTimeout
169+
}
170+
171+
/**
172+
* Stops the receive timeout for the channel.
173+
*/
174+
stopReceiveTimeout () {
175+
if (this._receiveTimeout !== null && this._receiveTimeoutStarted) {
176+
this._receiveTimeoutStarted = false
177+
if (this._receiveTimeoutId != null) {
178+
clearTimeout(this._receiveTimeoutId)
179+
}
180+
this._receiveTimeoutId = null
181+
}
182+
}
183+
184+
/**
185+
* Start the receive timeout for the channel.
186+
*/
187+
startReceiveTimeout () {
188+
if (this._receiveTimeout !== null && !this._receiveTimeoutStarted) {
189+
this._receiveTimeoutStarted = true
190+
this._resetTimeout()
191+
}
192+
}
193+
194+
_resetTimeout () {
195+
if (!this._receiveTimeoutStarted) {
196+
return
197+
}
198+
199+
if (this._receiveTimeoutId !== null) {
200+
clearTimeout(this._receiveTimeoutId)
201+
}
202+
203+
this._receiveTimeoutId = setTimeout(() => {
204+
this._receiveTimeoutId = null
205+
this.stopReceiveTimeout()
206+
this._error = newError(
207+
`Connection lost. Server didn't respond in ${this._receiveTimeout}ms`,
208+
this._config.connectionErrorCode
209+
)
210+
211+
this.close()
212+
.catch(() => {
213+
// ignoring error during the close timeout connections since they
214+
// not valid
215+
})
216+
.finally(() => {
217+
if (this.onerror) {
218+
this.onerror(this._error)
219+
}
220+
})
221+
}, this._receiveTimeout)
222+
}
223+
}
224+
225+
const TrustStrategy = {
226+
TRUST_CUSTOM_CA_SIGNED_CERTIFICATES: async function (config) {
227+
if (
228+
!config.trustedCertificates ||
229+
config.trustedCertificates.length === 0
230+
) {
231+
throw newError(
232+
'You are using TRUST_CUSTOM_CA_SIGNED_CERTIFICATES as the method ' +
233+
'to verify trust for encrypted connections, but have not configured any ' +
234+
'trustedCertificates. You must specify the path to at least one trusted ' +
235+
'X.509 certificate for this to work. Two other alternatives is to use ' +
236+
'TRUST_ALL_CERTIFICATES or to disable encryption by setting encrypted="' +
237+
ENCRYPTION_OFF +
238+
'"' +
239+
'in your driver configuration.'
240+
);
241+
}
242+
243+
const caCerts = await Promise.all(
244+
config.trustedCertificates.map(f => Deno.readTextFile(f))
245+
)
246+
247+
return Deno.connectTls({
248+
hostname: config.address.resolvedHost(),
249+
port: config.address.port(),
250+
caCerts
251+
})
252+
},
253+
TRUST_SYSTEM_CA_SIGNED_CERTIFICATES: function (config) {
254+
return Deno.connectTls({
255+
hostname: config.address.resolvedHost(),
256+
port: config.address.port()
257+
})
258+
},
259+
TRUST_ALL_CERTIFICATES: function (config) {
260+
throw newError(
261+
`"${config.trust}" is not available in DenoJS. ` +
262+
'For trust in any certificates, you should use the DenoJS flag ' +
263+
'"--unsafely-ignore-certificate-errors". '+
264+
'See, https://deno.com/blog/v1.13#disable-tls-verification'
265+
)
266+
}
267+
}
268+
269+
async function _connect (config) {
270+
if (!isEncrypted(config)) {
271+
return Deno.connect({
272+
hostname: config.address.resolvedHost(),
273+
port: config.address.port()
274+
})
275+
}
276+
const trustStrategyName = getTrustStrategyName(config)
277+
const trustStrategy = TrustStrategy[trustStrategyName]
278+
279+
if (trustStrategy != null) {
280+
return await trustStrategy(config)
281+
}
282+
283+
throw newError(
284+
'Unknown trust strategy: ' +
285+
config.trust +
286+
'. Please use either ' +
287+
"trust:'TRUST_CUSTOM_CA_SIGNED_CERTIFICATES' configuration " +
288+
'or the System CA. ' +
289+
'Alternatively, you can disable encryption by setting ' +
290+
'`encrypted:"' +
291+
ENCRYPTION_OFF +
292+
'"`. There is no mechanism to use encryption without trust verification, ' +
293+
'because this incurs the overhead of encryption without improving security. If ' +
294+
'the driver does not verify that the peer it is connected to is really Neo4j, it ' +
295+
'is very easy for an attacker to bypass the encryption by pretending to be Neo4j.'
296+
297+
)
298+
}
299+
300+
function isEncrypted (config) {
301+
const encryptionNotConfigured =
302+
config.encrypted == null || config.encrypted === undefined
303+
if (encryptionNotConfigured) {
304+
// default to using encryption if trust-all-certificates is available
305+
return false
306+
}
307+
return config.encrypted === true || config.encrypted === ENCRYPTION_ON
308+
}
309+
310+
function getTrustStrategyName (config) {
311+
if (config.trust) {
312+
return config.trust
313+
}
314+
return 'TRUST_SYSTEM_CA_SIGNED_CERTIFICATES'
315+
}
316+
317+
async function setupReader (channel) {
318+
try {
319+
for await (const message of iterateReader(channel._conn)) {
320+
channel._resetTimeout()
321+
322+
if (!channel._open) {
323+
return
324+
}
325+
if (channel.onmessage) {
326+
channel.onmessage(new ChannelBuffer(message))
327+
}
328+
}
329+
channel._handleConnectionTerminated()
330+
} catch (error) {
331+
if (channel._open) {
332+
channel._handleConnectionError(error)
333+
}
334+
}
335+
}
336+
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
import { internal } from 'neo4j-driver-core'
20+
21+
const {
22+
resolver: { BaseHostNameResolver }
23+
} = internal
24+
25+
export default class DenoHostNameResolver extends BaseHostNameResolver {
26+
resolve (address) {
27+
return this._resolveToItself(address)
28+
}
29+
}

0 commit comments

Comments
 (0)