18
18
*/
19
19
20
20
import net from 'net' ;
21
+ import tls from 'tls' ;
22
+ import fs from 'fs' ;
23
+ import path from 'path' ;
24
+ import { EOL } from 'os' ;
21
25
import { NodeBuffer } from './buf' ;
26
+ import { newError } from './error' ;
22
27
23
28
let _CONNECTION_IDGEN = 0 ;
24
29
25
- /**
26
- * In a Node.js environment the 'net' module is used
27
- * as transport.
28
- * @access private
29
- */
30
+ function userHome ( ) {
31
+ // For some reason, Browserify chokes on shimming `process`. This code
32
+ // will never get executed on the browser anyway, to just hack around it
33
+ let getOutOfHereBrowserifyYoureDrunk = require ;
34
+ let process = getOutOfHereBrowserifyYoureDrunk ( 'process' ) ;
35
+
36
+ return process . env [ ( process . platform == 'win32' ) ? 'USERPROFILE' : 'HOME' ] ;
37
+ }
38
+
39
+ function loadFingerprint ( serverId , knownHostsPath , cb ) {
40
+ if ( ! fs . existsSync ( knownHostsPath ) ) {
41
+ cb ( null ) ;
42
+ return ;
43
+ }
44
+ let found = false ;
45
+ require ( 'readline' ) . createInterface ( {
46
+ input : fs . createReadStream ( knownHostsPath )
47
+ } ) . on ( 'line' , ( line ) => {
48
+ if ( line . startsWith ( serverId ) ) {
49
+ found = true ;
50
+ cb ( line . split ( " " ) [ 1 ] ) ;
51
+ }
52
+ } ) . on ( 'close' , ( ) => {
53
+ if ( ! found ) {
54
+ cb ( null ) ;
55
+ }
56
+ } ) ;
57
+ }
58
+
59
+ function storeFingerprint ( serverId , knownHostsPath , fingerprint ) {
60
+ fs . appendFile ( knownHostsPath , serverId + " " + fingerprint + EOL , "utf8" ) ;
61
+ }
62
+
63
+ const TrustStrategy = {
64
+ TRUST_SIGNED_CERTIFICATES : function ( opts , onSuccess , onFailure ) {
65
+ if ( ! opts . trustedCertificates || opts . trustedCertificates . length == 0 ) {
66
+ onFailure ( newError ( "You are using TRUST_SIGNED_CERTIFICATES as the method " +
67
+ "to verify trust for encrypted connections, but have not configured any " +
68
+ "trustedCertificates. You must specify the path to at least one trusted " +
69
+ "X.509 certificate for this to work. Two other alternatives is to use " +
70
+ "TRUST_ON_FIRST_USE or to disable encryption by setting encrypted=false " +
71
+ "in your driver configuration." ) ) ;
72
+ return ;
73
+ }
74
+
75
+ let tlsOpts = {
76
+ ca : opts . trustedCertificates . map ( fs . readFileSync ) ,
77
+ // Because we manually check for this in the connect callback, to give
78
+ // a more helpful error to the user
79
+ rejectUnauthorized : false
80
+ } ;
81
+
82
+ let socket = tls . connect ( opts . port , opts . host , tlsOpts , function ( ) {
83
+ if ( ! socket . authorized ) {
84
+ onFailure ( newError ( "Server certificate is not trusted. If you trust the database you are connecting to, add" +
85
+ " the signing certificate, or the server certificate, to the list of certificates trusted by this driver" +
86
+ " using `neo4j.v1.driver(.., { trustedCertificates:['path/to/certificate.crt']}). This " +
87
+ " is a security measure to protect against man-in-the-middle attacks. If you are just trying " +
88
+ " Neo4j out and are not concerned about encryption, simply disable it using `encrypted=false` in the driver" +
89
+ " options." ) ) ;
90
+ } else {
91
+ onSuccess ( ) ;
92
+ }
93
+ } ) ;
94
+ return socket ;
95
+ } ,
96
+ TRUST_ON_FIRST_USE : function ( opts , onSuccess , onFailure ) {
97
+ let tlsOpts = {
98
+ // Because we manually verify the certificate against known_hosts
99
+ rejectUnauthorized : false
100
+ } ;
101
+
102
+ let socket = tls . connect ( opts . port , opts . host , tlsOpts , function ( ) {
103
+ var serverCert = socket . getPeerCertificate ( true ) ;
104
+
105
+ if ( ! serverCert . raw ) {
106
+ // If `raw` is not available, we're on an old version of NodeJS, and
107
+ // the raw cert cannot be accessed (or, at least I couldn't find a way to)
108
+ // therefore, we can't generate a SHA512 fingerprint, meaning we can't
109
+ // do TOFU, and the safe approach is to fail.
110
+ onFailure ( newError ( "You are using a version of NodeJS that does not " +
111
+ "support trust-on-first use encryption. You can either upgrade NodeJS to " +
112
+ "a newer version, use `trust:TRUST_SIGNED_CERTIFICATES` in your driver " +
113
+ "config instead, or disable encryption using `encrypted:false`." ) ) ;
114
+ return ;
115
+ }
116
+
117
+ var serverFingerprint = require ( 'crypto' ) . createHash ( 'sha512' ) . update ( serverCert . raw ) . digest ( "hex" ) ;
118
+ let knownHostsPath = opts . knownHosts || path . join ( userHome ( ) , ".neo4j" , "known_hosts" ) ;
119
+ let serverId = opts . host + ":" + opts . port ;
120
+
121
+ loadFingerprint ( serverId , knownHostsPath , ( knownFingerprint ) => {
122
+ if ( knownFingerprint === serverFingerprint ) {
123
+ onSuccess ( ) ;
124
+ } else if ( knownFingerprint == null ) {
125
+ storeFingerprint ( serverId , knownHostsPath , serverFingerprint ) ;
126
+ onSuccess ( ) ;
127
+ } else {
128
+ onFailure ( newError ( "Database encryption certificate has changed, and no longer " +
129
+ "matches the certificate stored for " + serverId + " in `" + knownHostsPath +
130
+ "`. As a security precaution, this driver will not automatically trust the new " +
131
+ "certificate, because doing so would allow an attacker to pretend to be the Neo4j " +
132
+ "instance we want to connect to. The certificate provided by the server looks like: " +
133
+ serverCert + ". If you trust that this certificate is valid, simply remove the line " +
134
+ "starting with " + serverId + " in `" + knownHostsPath + "`, and the driver will " +
135
+ "update the file with the new certificate. You can configure which file the driver " +
136
+ "should use to store this information by setting `knownHosts` to another path in " +
137
+ "your driver configuration - and you can disable encryption there as well using " +
138
+ "`encrypted:false`." ) )
139
+ }
140
+ } ) ;
141
+ } ) ;
142
+ return socket ;
143
+ }
144
+ } ;
145
+
146
+ function connect ( opts , onSuccess , onFailure = ( ( ) => null ) ) {
147
+ if ( opts . encrypted === false ) {
148
+ return net . connect ( opts . port , opts . host , onSuccess ) ;
149
+ } else if ( TrustStrategy [ opts . trust ] ) {
150
+ return TrustStrategy [ opts . trust ] ( opts , onSuccess , onFailure ) ;
151
+ } else {
152
+ onFailure ( newError ( "Unknown trust strategy: " + opts . trust + ". Please use either " +
153
+ "trust:'TRUST_SIGNED_CERTIFICATES' or trust:'TRUST_ON_FIRST_USE' in your driver " +
154
+ "configuration. Alternatively, you can disable encryption by setting " +
155
+ "`encrypted:false`. There is no mechanism to use encryption without trust verification, " +
156
+ "because this incurs the overhead of encryption without improving security. If " +
157
+ "the driver does not verify that the peer it is connected to is really Neo4j, it " +
158
+ "is very easy for an attacker to bypass the encryption by pretending to be Neo4j." ) ) ;
159
+ }
160
+ }
30
161
162
+ /**
163
+ * In a Node.js environment the 'net' module is used
164
+ * as transport.
165
+ * @access private
166
+ */
31
167
class NodeChannel {
32
168
33
169
/**
@@ -37,35 +173,42 @@ class NodeChannel {
37
173
* @param {Integer } opts.port - The port to use.
38
174
*/
39
175
constructor ( opts ) {
40
- let _self = this ;
176
+ let self = this ;
41
177
42
178
this . id = _CONNECTION_IDGEN ++ ;
43
179
this . available = true ;
44
180
this . _pending = [ ] ;
45
181
this . _open = true ;
46
- this . _conn = net . connect ( ( opts . port || 7687 ) , opts . host , ( ) => {
47
- if ( ! _self . _open ) {
182
+ this . _error = null ;
183
+ this . _handleConnectionError = this . _handleConnectionError . bind ( this ) ;
184
+
185
+ this . _conn = connect ( opts , ( ) => {
186
+ if ( ! self . _open ) {
48
187
return ;
49
188
}
189
+
190
+ self . _conn . on ( 'data' , ( buffer ) => {
191
+ if ( self . onmessage ) {
192
+ self . onmessage ( new NodeBuffer ( buffer ) ) ;
193
+ }
194
+ } ) ;
195
+
196
+ self . _conn . on ( 'error' , self . _handleConnectionError ) ;
197
+
50
198
// Drain all pending messages
51
- let pending = _self . _pending ;
52
- _self . _pending = null ;
199
+ let pending = self . _pending ;
200
+ self . _pending = null ;
53
201
for ( let i = 0 ; i < pending . length ; i ++ ) {
54
- _self . write ( pending [ i ] ) ;
202
+ self . write ( pending [ i ] ) ;
55
203
}
56
- } ) ;
57
-
58
- this . _conn . on ( 'data' , ( buffer ) => {
59
- if ( _self . onmessage ) {
60
- _self . onmessage ( new NodeBuffer ( buffer ) ) ;
61
- }
62
- } ) ;
204
+ } , this . _handleConnectionError ) ;
205
+ }
63
206
64
- this . _conn . on ( 'error' , function ( err ) {
65
- if ( _self . onerror ) {
66
- _self . onerror ( err ) ;
67
- }
68
- } ) ;
207
+ _handleConnectionError ( err ) {
208
+ this . _error = err ;
209
+ if ( this . onerror ) {
210
+ this . onerror ( err ) ;
211
+ }
69
212
}
70
213
71
214
/**
@@ -89,12 +232,14 @@ class NodeChannel {
89
232
* Close the connection
90
233
* @param {function } cb - Function to call on close.
91
234
*/
92
- close ( cb ) {
93
- if ( cb ) {
235
+ close ( cb = ( ( ) => null ) ) {
236
+ this . _open = false ;
237
+ if ( this . _conn ) {
238
+ this . _conn . end ( ) ;
94
239
this . _conn . on ( 'end' , cb ) ;
240
+ } else {
241
+ cb ( ) ;
95
242
}
96
- this . _open = false ;
97
- this . _conn . end ( ) ;
98
243
}
99
244
}
100
245
let _nodeChannelModule = { channel : NodeChannel , available : true } ;
0 commit comments