@@ -20,8 +20,11 @@ package bolt
2020
2121import (
2222 "context"
23+ "encoding/binary"
2324 "fmt"
25+ "io"
2426 "net"
27+ "strings"
2528
2629 "github.com/neo4j/neo4j-go-driver/v5/neo4j/internal/db"
2730 "github.com/neo4j/neo4j-go-driver/v5/neo4j/internal/errorutil"
@@ -35,12 +38,18 @@ type protocolVersion struct {
3538 back byte // Number of minor versions back
3639}
3740
38- // Supported versions in priority order
41+ func (p * protocolVersion ) formatProtocol () string {
42+ return fmt .Sprintf ("%#04X%02X%02X" , p .back , p .minor , p .major )
43+ }
44+
45+ // versions lists the supported protocol versions in priority order.
46+ // The first proposal is a marker indicating that the client wishes to use the
47+ // new manifest-style negotiation.
3948var versions = [4 ]protocolVersion {
49+ {major : 0xFF , minor : 0x01 , back : 0x00 }, // Bolt manifest marker
4050 {major : 5 , minor : 7 , back : 7 },
4151 {major : 4 , minor : 4 , back : 2 },
42- {major : 4 , minor : 1 },
43- {major : 3 , minor : 0 },
52+ {major : 3 , minor : 0 , back : 0 },
4453}
4554
4655// Connect initiates the negotiation of the Bolt protocol version.
@@ -70,6 +79,7 @@ func Connect(ctx context.Context,
7079 boltLogger .LogClientMessage ("" , "<MAGIC> %#010X" , handshake [0 :4 ])
7180 boltLogger .LogClientMessage ("" , "<HANDSHAKE> %#010X %#010X %#010X %#010X" , handshake [4 :8 ], handshake [8 :12 ], handshake [12 :16 ], handshake [16 :20 ])
7281 }
82+ // Write handshake proposals to server
7383 _ , err := racing .NewRacingWriter (conn ).Write (ctx , handshake )
7484 if err != nil {
7585 errorListener .OnDialError (ctx , serverName , err )
@@ -84,14 +94,29 @@ func Connect(ctx context.Context,
8494 return nil , err
8595 }
8696
87- if boltLogger != nil {
88- boltLogger .LogServerMessage ("" , "<HANDSHAKE> %#010X" , buf )
89- }
90-
9197 major := buf [3 ]
9298 minor := buf [2 ]
9399
100+ if major == 80 && minor == 84 {
101+ return nil , & errorutil.UsageError {Message : "server responded HTTP. Make sure you are not trying to connect to the http endpoint " +
102+ "(HTTP defaults to port 7474 whereas BOLT defaults to port 7687)" }
103+ }
104+
105+ // Log legacy handshake response.
106+ if ! (major == 0xFF && minor == 0x01 ) && boltLogger != nil {
107+ boltLogger .LogServerMessage ("" , "<HANDSHAKE> %#010X" , buf )
108+ }
109+
94110 bufferedConn := bufferedConnection (conn , readBufferSize )
111+
112+ // If the server selected manifest negotiation, perform the extended handshake.
113+ if major == 0xFF && minor == 0x01 {
114+ major , minor , err = performManifestNegotiation (ctx , bufferedConn , serverName , errorListener , boltLogger , buf )
115+ if err != nil {
116+ return nil , err
117+ }
118+ }
119+
95120 var boltConn db.Connection
96121 switch major {
97122 case 3 :
@@ -103,10 +128,6 @@ func Connect(ctx context.Context,
103128 case 0 :
104129 return nil , fmt .Errorf ("server did not accept any of the requested Bolt versions (%#v)" , versions )
105130 default :
106- if major == 80 && minor == 84 {
107- return nil , & errorutil.UsageError {Message : "server responded HTTP. Make sure you are not trying to connect to the http endpoint " +
108- "(HTTP defaults to port 7474 whereas BOLT defaults to port 7687)" }
109- }
110131 return nil , & errorutil.UsageError {Message : fmt .Sprintf ("server responded with unsupported version %d.%d" , major , minor )}
111132 }
112133 if err = boltConn .Connect (ctx , int (minor ), auth , userAgent , routingContext , notificationConfig ); err != nil {
@@ -115,3 +136,165 @@ func Connect(ctx context.Context,
115136 }
116137 return boltConn , nil
117138}
139+
140+ // performManifestNegotiation handles the manifest-style handshake.
141+ // Returns the negotiated protocol's major and minor version.
142+ func performManifestNegotiation (
143+ ctx context.Context ,
144+ conn io.ReadWriteCloser ,
145+ serverName string ,
146+ errorListener ConnectionErrorListener ,
147+ boltLogger log.BoltLogger ,
148+ response []byte ,
149+ ) (byte , byte , error ) {
150+ reader := racing .NewRacingReader (conn )
151+
152+ // Read the protocol offerings.
153+ supported , err := readProtocolOfferings (ctx , reader , serverName , errorListener )
154+ if err != nil {
155+ return 0 , 0 , err
156+ }
157+
158+ // Read the capability mask.
159+ _ , capBytes , err := readCapabilityMask (ctx , reader , serverName , errorListener )
160+ if err != nil {
161+ return 0 , 0 , err
162+ }
163+
164+ // Log the complete server handshake message.
165+ logManifestHandshake (boltLogger , response , supported , capBytes )
166+
167+ // Select an acceptable protocol version.
168+ chosen , err := selectProtocol (supported )
169+ if err != nil {
170+ errorListener .OnDialError (ctx , serverName , err )
171+ // Best-effort attempt to send an invalid handshake (ignore any error).
172+ invalidHandshake := []byte {0x00 , 0x00 , 0x00 , 0x00 , 0x00 } // 4 bytes for version + 1 byte for capabilities.
173+ _ , _ = racing .NewRacingWriter (conn ).Write (ctx , invalidHandshake )
174+ return 0 , 0 , err
175+ }
176+
177+ // Send the handshake confirmation.
178+ if err = sendHandshakeConfirmation (ctx , conn , boltLogger , errorListener , serverName , chosen , []byte {0x00 }); err != nil {
179+ return 0 , 0 , err
180+ }
181+
182+ return chosen .major , chosen .minor , nil
183+ }
184+
185+ // readProtocolOfferings reads the number of protocol offerings and returns the count and
186+ // a slice of supported protocol versions.
187+ func readProtocolOfferings (ctx context.Context , r racing.RacingReader , serverName string , errorListener ConnectionErrorListener ) ([]protocolVersion , error ) {
188+ count , _ , err := readVarInt (ctx , r )
189+ if err != nil {
190+ errorListener .OnDialError (ctx , serverName , err )
191+ return nil , fmt .Errorf ("failed to read manifest protocol count: %w" , err )
192+ }
193+ supported := make ([]protocolVersion , 0 , count )
194+ for i := uint64 (0 ); i < count ; i ++ {
195+ var versionBytes [4 ]byte
196+ _ , err := r .ReadFull (ctx , versionBytes [:])
197+ if err != nil {
198+ errorListener .OnDialError (ctx , serverName , err )
199+ return nil , fmt .Errorf ("failed to read manifest protocol version: %w" , err )
200+ }
201+ supported = append (supported , protocolVersion {
202+ back : versionBytes [1 ],
203+ minor : versionBytes [2 ],
204+ major : versionBytes [3 ],
205+ })
206+ }
207+ return supported , nil
208+ }
209+
210+ // readCapabilityMask reads the capability bit mask (a Base128 VarInt) and returns both the
211+ // raw value and its encoded byte slice.
212+ func readCapabilityMask (ctx context.Context , r racing.RacingReader , serverName string , errorListener ConnectionErrorListener ) (uint64 , []byte , error ) {
213+ capMask , capBytes , err := readVarInt (ctx , r )
214+ if err != nil {
215+ errorListener .OnDialError (ctx , serverName , err )
216+ return 0 , capBytes , fmt .Errorf ("failed to read capability mask: %w" , err )
217+ }
218+ return capMask , capBytes , nil
219+ }
220+
221+ // logManifestHandshake logs the complete server handshake message for manifest negotiation.
222+ // It prints the initial response, count of offerings, each supported protocol, and the capability mask.
223+ func logManifestHandshake (boltLogger log.BoltLogger , response []byte , supported []protocolVersion , capBytes []byte ) {
224+ if boltLogger == nil {
225+ return
226+ }
227+ supportedProtocols := make ([]string , 0 , len (supported ))
228+ for _ , p := range supported {
229+ supportedProtocols = append (supportedProtocols , p .formatProtocol ())
230+ }
231+ boltLogger .LogServerMessage ("" , "<HANDSHAKE> %s [%d] %s %s" ,
232+ fmt .Sprintf ("%#X" , response ),
233+ len (supported ),
234+ strings .Join (supportedProtocols , " " ),
235+ fmt .Sprintf ("%#X" , capBytes ))
236+ }
237+
238+ // selectProtocol iterates over our protocol proposals (skipping the manifest marker)
239+ // and returns the first candidate whose major version matches and whose minor version
240+ // falls within the range offered by the server.
241+ func selectProtocol (offers []protocolVersion ) (protocolVersion , error ) {
242+ for _ , candidate := range versions [1 :] {
243+ for v := int (candidate .minor ); v >= int (candidate .minor )- int (candidate .back ); v -- {
244+ for _ , offer := range offers {
245+ if offer .major != candidate .major {
246+ continue
247+ }
248+ if byte (v ) <= offer .minor && byte (v ) >= offer .minor - offer .back {
249+ return protocolVersion {major : candidate .major , minor : byte (v )}, nil
250+ }
251+ }
252+ }
253+ }
254+ return protocolVersion {}, fmt .Errorf ("none of the server offered Bolt versions are supported (offered: %#v)" , offers )
255+ }
256+
257+ // sendHandshakeConfirmation sends the chosen protocol version and capability mask back to the server.
258+ func sendHandshakeConfirmation (ctx context.Context , conn io.ReadWriteCloser , boltLogger log.BoltLogger , errorListener ConnectionErrorListener , serverName string , chosen protocolVersion , capBytes []byte ) error {
259+ chosenBytes := []byte {0x00 , 0x00 , chosen .minor , chosen .major }
260+ if boltLogger != nil {
261+ boltLogger .LogClientMessage ("" , "<HANDSHAKE> %#X %#X" , chosenBytes , capBytes )
262+ }
263+ writer := racing .NewRacingWriter (conn )
264+ if _ , err := writer .Write (ctx , chosenBytes ); err != nil {
265+ errorListener .OnDialError (ctx , serverName , err )
266+ return err
267+ }
268+ if _ , err := writer .Write (ctx , capBytes ); err != nil {
269+ errorListener .OnDialError (ctx , serverName , err )
270+ return err
271+ }
272+ return nil
273+ }
274+
275+ // readVarInt returns a Base128-encoded variable-length integer from the reader.
276+ func readVarInt (ctx context.Context , r racing.RacingReader ) (uint64 , []byte , error ) {
277+ var buf [binary .MaxVarintLen64 ]byte
278+ // Read one byte at a time until a byte with the MSB not set is encountered.
279+ for i := 0 ; i < len (buf ); i ++ {
280+ if _ , err := r .Read (ctx , buf [i :i + 1 ]); err != nil {
281+ return 0 , buf [:i ], err
282+ }
283+ // If the continuation bit is not set, we've reached the end of the varint.
284+ if buf [i ]& 0x80 == 0 {
285+ value , n := binary .Uvarint (buf [:i + 1 ])
286+ if n <= 0 {
287+ return 0 , buf [:i + 1 ], fmt .Errorf ("failed to decode varint" )
288+ }
289+ return value , buf [:i + 1 ], nil
290+ }
291+ }
292+ return 0 , buf [:], fmt .Errorf ("varint too long" )
293+ }
294+
295+ // encodeVarInt returns the encoded unsigned integer into a Base128 variable-length integer.
296+ func encodeVarInt (value uint64 ) []byte {
297+ var buf [binary .MaxVarintLen64 ]byte
298+ n := binary .PutUvarint (buf [:], value )
299+ return buf [:n ]
300+ }
0 commit comments