// Copyright (C) MongoDB, Inc. 2021-present. // // Licensed under the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. You may obtain // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 package operation import ( "context" "errors" "runtime" "strconv" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/internal" "go.mongodb.org/mongo-driver/mongo/address" "go.mongodb.org/mongo-driver/mongo/description" "go.mongodb.org/mongo-driver/version" "go.mongodb.org/mongo-driver/x/bsonx/bsoncore" "go.mongodb.org/mongo-driver/x/mongo/driver" "go.mongodb.org/mongo-driver/x/mongo/driver/session" ) // Hello is used to run the handshake operation. type Hello struct { appname string compressors []string saslSupportedMechs string d driver.Deployment clock *session.ClusterClock speculativeAuth bsoncore.Document topologyVersion *description.TopologyVersion maxAwaitTimeMS *int64 serverAPI *driver.ServerAPIOptions loadBalanced bool res bsoncore.Document } var _ driver.Handshaker = (*Hello)(nil) // NewHello constructs a Hello. func NewHello() *Hello { return &Hello{} } // AppName sets the application name in the client metadata sent in this operation. func (h *Hello) AppName(appname string) *Hello { h.appname = appname return h } // ClusterClock sets the cluster clock for this operation. func (h *Hello) ClusterClock(clock *session.ClusterClock) *Hello { if h == nil { h = new(Hello) } h.clock = clock return h } // Compressors sets the compressors that can be used. func (h *Hello) Compressors(compressors []string) *Hello { h.compressors = compressors return h } // SASLSupportedMechs retrieves the supported SASL mechanism for the given user when this operation // is run. func (h *Hello) SASLSupportedMechs(username string) *Hello { h.saslSupportedMechs = username return h } // Deployment sets the Deployment for this operation. func (h *Hello) Deployment(d driver.Deployment) *Hello { h.d = d return h } // SpeculativeAuthenticate sets the document to be used for speculative authentication. func (h *Hello) SpeculativeAuthenticate(doc bsoncore.Document) *Hello { h.speculativeAuth = doc return h } // TopologyVersion sets the TopologyVersion to be used for heartbeats. func (h *Hello) TopologyVersion(tv *description.TopologyVersion) *Hello { h.topologyVersion = tv return h } // MaxAwaitTimeMS sets the maximum time for the server to wait for topology changes during a heartbeat. func (h *Hello) MaxAwaitTimeMS(awaitTime int64) *Hello { h.maxAwaitTimeMS = &awaitTime return h } // ServerAPI sets the server API version for this operation. func (h *Hello) ServerAPI(serverAPI *driver.ServerAPIOptions) *Hello { h.serverAPI = serverAPI return h } // LoadBalanced specifies whether or not this operation is being sent over a connection to a load balanced cluster. func (h *Hello) LoadBalanced(lb bool) *Hello { h.loadBalanced = lb return h } // Result returns the result of executing this operation. func (h *Hello) Result(addr address.Address) description.Server { return description.NewServer(addr, bson.Raw(h.res)) } // handshakeCommand appends all necessary command fields as well as client metadata, SASL supported mechs, and compression. func (h *Hello) handshakeCommand(dst []byte, desc description.SelectedServer) ([]byte, error) { dst, err := h.command(dst, desc) if err != nil { return dst, err } if h.saslSupportedMechs != "" { dst = bsoncore.AppendStringElement(dst, "saslSupportedMechs", h.saslSupportedMechs) } if h.speculativeAuth != nil { dst = bsoncore.AppendDocumentElement(dst, "speculativeAuthenticate", h.speculativeAuth) } var idx int32 idx, dst = bsoncore.AppendArrayElementStart(dst, "compression") for i, compressor := range h.compressors { dst = bsoncore.AppendStringElement(dst, strconv.Itoa(i), compressor) } dst, _ = bsoncore.AppendArrayEnd(dst, idx) // append client metadata idx, dst = bsoncore.AppendDocumentElementStart(dst, "client") didx, dst := bsoncore.AppendDocumentElementStart(dst, "driver") dst = bsoncore.AppendStringElement(dst, "name", "mongo-go-driver") dst = bsoncore.AppendStringElement(dst, "version", version.Driver) dst, _ = bsoncore.AppendDocumentEnd(dst, didx) didx, dst = bsoncore.AppendDocumentElementStart(dst, "os") dst = bsoncore.AppendStringElement(dst, "type", runtime.GOOS) dst = bsoncore.AppendStringElement(dst, "architecture", runtime.GOARCH) dst, _ = bsoncore.AppendDocumentEnd(dst, didx) dst = bsoncore.AppendStringElement(dst, "platform", runtime.Version()) if h.appname != "" { didx, dst = bsoncore.AppendDocumentElementStart(dst, "application") dst = bsoncore.AppendStringElement(dst, "name", h.appname) dst, _ = bsoncore.AppendDocumentEnd(dst, didx) } dst, _ = bsoncore.AppendDocumentEnd(dst, idx) return dst, nil } // command appends all necessary command fields. func (h *Hello) command(dst []byte, desc description.SelectedServer) ([]byte, error) { // Use "hello" if topology is LoadBalanced, API version is declared or server // has responded with "helloOk". Otherwise, use legacy hello. if desc.Kind == description.LoadBalanced || h.serverAPI != nil || desc.Server.HelloOK { dst = bsoncore.AppendInt32Element(dst, "hello", 1) } else { dst = bsoncore.AppendInt32Element(dst, internal.LegacyHello, 1) } dst = bsoncore.AppendBooleanElement(dst, "helloOk", true) if tv := h.topologyVersion; tv != nil { var tvIdx int32 tvIdx, dst = bsoncore.AppendDocumentElementStart(dst, "topologyVersion") dst = bsoncore.AppendObjectIDElement(dst, "processId", tv.ProcessID) dst = bsoncore.AppendInt64Element(dst, "counter", tv.Counter) dst, _ = bsoncore.AppendDocumentEnd(dst, tvIdx) } if h.maxAwaitTimeMS != nil { dst = bsoncore.AppendInt64Element(dst, "maxAwaitTimeMS", *h.maxAwaitTimeMS) } if h.loadBalanced { // The loadBalanced parameter should only be added if it's true. We should never explicitly send // loadBalanced=false per the load balancing spec. dst = bsoncore.AppendBooleanElement(dst, "loadBalanced", true) } return dst, nil } // Execute runs this operation. func (h *Hello) Execute(ctx context.Context) error { if h.d == nil { return errors.New("a Hello must have a Deployment set before Execute can be called") } return h.createOperation().Execute(ctx) } // StreamResponse gets the next streaming Hello response from the server. func (h *Hello) StreamResponse(ctx context.Context, conn driver.StreamerConnection) error { return h.createOperation().ExecuteExhaust(ctx, conn) } func (h *Hello) createOperation() driver.Operation { return driver.Operation{ Clock: h.clock, CommandFn: h.command, Database: "admin", Deployment: h.d, ProcessResponseFn: func(info driver.ResponseInfo) error { h.res = info.ServerResponse return nil }, ServerAPI: h.serverAPI, } } // GetHandshakeInformation performs the MongoDB handshake for the provided connection and returns the relevant // information about the server. This function implements the driver.Handshaker interface. func (h *Hello) GetHandshakeInformation(ctx context.Context, _ address.Address, c driver.Connection) (driver.HandshakeInformation, error) { err := driver.Operation{ Clock: h.clock, CommandFn: h.handshakeCommand, Deployment: driver.SingleConnectionDeployment{c}, Database: "admin", ProcessResponseFn: func(info driver.ResponseInfo) error { h.res = info.ServerResponse return nil }, ServerAPI: h.serverAPI, }.Execute(ctx) if err != nil { return driver.HandshakeInformation{}, err } info := driver.HandshakeInformation{ Description: h.Result(c.Address()), } if speculativeAuthenticate, ok := h.res.Lookup("speculativeAuthenticate").DocumentOK(); ok { info.SpeculativeAuthenticate = speculativeAuthenticate } if serverConnectionID, ok := h.res.Lookup("connectionId").AsInt64OK(); ok { info.ServerConnectionID = &serverConnectionID } // Cast to bson.Raw to lookup saslSupportedMechs to avoid converting from bsoncore.Value to bson.RawValue for the // StringSliceFromRawValue call. if saslSupportedMechs, lookupErr := bson.Raw(h.res).LookupErr("saslSupportedMechs"); lookupErr == nil { info.SaslSupportedMechs, err = internal.StringSliceFromRawValue("saslSupportedMechs", saslSupportedMechs) } return info, err } // FinishHandshake implements the Handshaker interface. This is a no-op function because a non-authenticated connection // does not do anything besides the initial Hello for a handshake. func (h *Hello) FinishHandshake(context.Context, driver.Connection) error { return nil }